- PVSM.RU - https://www.pvsm.ru -
В последнее время на Хабре часто начали появляться стати о ASP.NET MVC. Однако в этой статье я бы хотел сделать несколько заметок по поводу построения приложений на вышеприведенном фреймворке: минимальный набор NuGet-packages (без которых грех начинать работу), логирование, подводные камни при использовании стандартных membership-, profile- провайдеров. И, напоследок, почему Web API из MVC 4 — то, что так долго мы все ждали.
Итак, определимся без каких пакетов нельзя начинать разрабатывать веб-приложение на ASP.NET MVC. В нижеприведенном списке хоть и находятся те [пакеты], которые по-умолчанию ставятся при создании решения, но я их все же включу.
Entity Framework 4.1 [1] — возникает вопрос, почему он? Ну что ж поясню на примере. Существует достаточное количество других схожих, превосходящих и пр. ORM-фреймворков (один NHibernate чего стоит). Еще пару лет назад я бы рекомендовал для начала использовать легковесный (относительно, судя по синтетическим тестам) LINQ to SQL. НО! Выход Entity Framework 4.1 вместе с Code First перевесил все минусы: прототипирование слоя данных приложений стало одним удовольствием. Если для первого нужно работать в дизайнере, иметь дело с DBML-файлами, то здесь лишь работаем с POCO. Например, модель данных для магазина:
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int Price { get; set; }
public DateTime CreationDate { get; set; }
public string Description { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public class ProductsContext : DbContext
{
public DbSet<Category> Categories { get; set; }
}
MvcScaffolding [2] — нужно быстро набросать CRUD-панель? Уже есть модель EF, либо LINQ to SQL? Тогда введите данную команду в окно NuGet и возрадуйтесь кодогенерации:
Scaffold Controller [имя модели] –Repository
Флаг –Repository позволяет заодно и создать репозиторий для работы со слоем данных.
Для примера используем вышеприведенную модель.
После ввода
Scaffold Controller Product –Repository
будут сгенерированы следующие CRUD-страницы и абстрактный репозиторий:
public interface IProductRepository
{
IQueryable<Product> All { get; }
IQueryable<Product> AllIncluding(params Expression<Func<Product, object>>[] includeProperties);
Product Find(int id);
void InsertOrUpdate(Product product);
void Delete(int id);
void Save();
}
А также его реализация:
public class ProductRepository : IProductRepository
{
ProductsContext context = new ProductsContext();
public IQueryable<Product> All
{
get { return context.Products; }
}
public IQueryable<Product> AllIncluding(params Expression<Func<Product, object>>[] includeProperties)
{
IQueryable<Product> query = context.Products;
foreach (var includeProperty in includeProperties) {
query = query.Include(includeProperty);
}
return query;
}
public Product Find(int id)
{
return context.Products.Find(id);
}
public void InsertOrUpdate(Product product)
{
if (product.ProductId == default(int)) {
// New entity
context.Products.Add(product);
} else {
// Existing entity
context.Entry(product).State = EntityState.Modified;
}
}
public void Delete(int id)
{
var product = context.Products.Find(id);
context.Products.Remove(product);
}
public void Save()
{
context.SaveChanges();
}
}
Для более детального ознакомления советую почитать серию статей [3] от самих создателей.
Ninject [4] — лично мне не представляется возможность работать без абстракций. ASP.NET MVC имеет множество возможностей контроля/расширения функционала своих фабрик. Поэтому завязывание функционала на конкретных реализациях классов — плохой тон. Почему Ninject? Ответ прост — он легковесен, имеет множество расширений, активно развивается.
Установим его, а также дополнение к нему MVC3 [5]:
После этого появится папка App_Start, где будет располагаться файл NinjectMVC3.cs.
Для реализации DI создадим модуль:
class RepoModule : NinjectModule
{
public override void Load()
{
Bind<ICategoryRepository>().To<CategoryRepository>();
Bind<IProductRepository>().To<ProductRepository>();
}
}
В файле NinjectMVC3.cs в методе CreateKernel запишем:
var modules = new INinjectModule[]
{
new RepoModule()
};
var kernel = new StandardKernel(modules);
RegisterServices(kernel);
return kernel;
Теперь напишем наш контроллер:
public class ProductsController : Controller
{
private readonly IProductRepository productRepository;
public ProductsController(IProductRepository productRepository)
{
this.productRepository = productRepository;
}
}
NLog [6] — как узнать, как работает приложение, успехи/неудачи при выполнении операций? Самое простое решение — использовать логирование. Писать свои велосипеды смысла нет. Из всех, думаю, можно выделить NLog и log4net. Последний является прямым портом с Java (log4j). Но его развитие не очень активное, если не заброшено вообще. NLog наоборот активно развивается, имеет богатый функционал и простой API.
Как быстро добавить логгер:
public class ProductController : Controller
{
private static Logger log = LogManager.GetCurrentClassLogger();
public ActionResult DoStuff()
{
//very important stuff
log.Info("Everything is OK!");
return View();
}
}
PagedList [7] — нужен алгоритм «листания страниц»? Да можно самому посидеть и придумать. Но зачем? В этой [8] статье есть детальное описание работы с ним.
Lucene.NET [9] — Вы все еще стираете используете поиск самой БД? Забудьте! Пара минут и у Вас появится сверхскоростной поиск.
Установим его, а также дополнение к нему SimpleLucene [10]:
Первым делом автоматизируем работу с созданием индекса:
public class ProductIndexDefinition : IIndexDefinition<Product>
{
public Document Convert(Product entity)
{
var document = new Document();
document.Add(new Field("ProductId", entity.ProductId.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
document.Add(new Field("Name", entity.Name, Field.Store.YES, Field.Index.ANALYZED));
if (!string.IsNullOrEmpty(entity.Description))
{
document.Add(new Field("Description", entity.Description, Field.Store.YES, Field.Index.ANALYZED));
}
document.Add(new Field("CreationDate", DateTools.DateToString(entity.CreationDate, DateTools.Resolution.DAY),
Field.Store.YES, Field.Index.NOT_ANALYZED));
if (entity.Price != null)
{
var priceField = new NumericField("Price", Field.Store.YES, true);
priceField.SetIntValue(entity.Price);
document.Add(priceField);
}
document.Add(new Field("CategoryId", entity.CategoryId.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
return document;
}
public Term GetIndex(Product entity)
{
return new Term("ProductId", entity.ProductId.ToString());
}
}
Как видно в методе Convert мы сериализуем POCO в Lucene Document.
Код контроллера:
public ActionResult Create(Product product)
{
if (ModelState.IsValid) {
product.CreationDate = DateTime.Now;
productRepository.InsertOrUpdate(product);
productRepository.Save();
// index location
var indexLocation = new FileSystemIndexLocation(new DirectoryInfo(Server.MapPath("~/Index")));
var definition = new ProductIndexDefinition();
var task = new EntityUpdateTask<Product>(product, definition, indexLocation);
task.IndexOptions.RecreateIndex = false;
task.IndexOptions.OptimizeIndex = true;
//IndexQueue.Instance.Queue(task);
var indexWriter = new DirectoryIndexWriter(new DirectoryInfo(Server.MapPath("~/Index")), false);
using (var indexService = new IndexService(indexWriter))
{
task.Execute(indexService);
}
return RedirectToAction("Index");
} else {
ViewBag.PossibleCategories = categoryRepository.All;
return View();
}
}
Для вывода результатов создадим ResultDefinition:
public class ProductResultDefinition : IResultDefinition<Product>
{
public Product Convert(Document document)
{
var product = new Product();
product.ProductId = document.GetValue<int>("ProductId");
product.Name = document.GetValue("Name");
product.Price = document.GetValue<int>("Price");
product.CategoryId = document.GetValue<int>("CategoryId");
product.CreationDate = DateTools.StringToDate(document.GetValue("CreationDate"));
product.Description = document.GetValue("Description");
return product;
}
}
Здесь происходит десериализация POCO.
И, наконец, автоматизируем работу с запросами:
public class ProductQuery : QueryBase
{
public ProductQuery(Query query) : base(query) { }
public ProductQuery() { }
public ProductQuery WithKeywords(string keywords)
{
if (!string.IsNullOrEmpty(keywords))
{
string[] fields = { "Name", "Description" };
var parser = new MultiFieldQueryParser(Lucene.Net.Util.Version.LUCENE_29,
fields, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29));
Query multiQuery = parser.Parse(keywords);
this.AddQuery(multiQuery);
}
return this;
}
}
}
Теперь перейдем к контроллеру:
public ActionResult Search(string searchText, bool? orderByDate)
{
string IndexPath = Server.MapPath("~/Index");
var indexSearcher = new DirectoryIndexSearcher(new DirectoryInfo(IndexPath), true);
using (var searchService = new SearchService(indexSearcher))
{
var query = new ProductQuery().WithKeywords(searchText);
var result = searchService.SearchIndex<Product>(query.Query, new ProductResultDefinition());
if (orderByDate.HasValue)
{
return View(result.Results.OrderBy(x => x.CreationDate).ToList())
}
return View(result.Results.ToList());
}
}
Reactive Extensions for JS [11] — должен быть основой клиента. Нет, честно, более плавного создания каркаса приложения на клиенте с возможностью юнит-тестирования надо еще поискать [12]. Советую почитать мой пост [13] по разработке на Rx.
Сразу же предупреждаю – никогда не используйте стандартный AspNetMembershipProvider! Если посмотреть на его монструозные хранимые процедуры из коробки, то просто захочется его выкинуть.
Откройте в папке C:WindowsMicrosoft.NETFrameworkv4.0.30319 файлы InstallMembership.sql и InstallProfile.SQL.
Например, вот так выглядит SQL-код для FindUsersByName из InstallMembership.sql:
CREATE PROCEDURE dbo.aspnet_Membership_FindUsersByName
@ApplicationName nvarchar(256),
@UserNameToMatch nvarchar(256),
@PageIndex int,
@PageSize int
AS
BEGIN
DECLARE @ApplicationId uniqueidentifier
SELECT @ApplicationId = NULL
SELECT @ApplicationId = ApplicationId FROM dbo.aspnet_Applications WHERE LOWER(@ApplicationName) = LoweredApplicationName
IF (@ApplicationId IS NULL)
RETURN 0
-- Set the page bounds
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @TotalRecords int
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageSize - 1 + @PageLowerBound
-- Create a temp table TO store the select results
CREATE TABLE #PageIndexForUsers
(
IndexId int IDENTITY (0, 1) NOT NULL,
UserId uniqueidentifier
)
-- Insert into our temp table
INSERT INTO #PageIndexForUsers (UserId)
SELECT u.UserId
FROM dbo.aspnet_Users u, dbo.aspnet_Membership m
WHERE u.ApplicationId = @ApplicationId AND m.UserId = u.UserId AND u.LoweredUserName LIKE LOWER(@UserNameToMatch)
ORDER BY u.UserName
SELECT u.UserName, m.Email, m.PasswordQuestion, m.Comment, m.IsApproved,
m.CreateDate,
m.LastLoginDate,
u.LastActivityDate,
m.LastPasswordChangedDate,
u.UserId, m.IsLockedOut,
m.LastLockoutDate
FROM dbo.aspnet_Membership m, dbo.aspnet_Users u, #PageIndexForUsers p
WHERE u.UserId = p.UserId AND u.UserId = m.UserId AND
p.IndexId >= @PageLowerBound AND p.IndexId <= @PageUpperBound
ORDER BY u.UserName
SELECT @TotalRecords = COUNT(*)
FROM #PageIndexForUsers
RETURN @TotalRecords
END
А вот так Profile_GetProfiles из InstallProfile.SQL:
CREATE PROCEDURE dbo.aspnet_Profile_GetProfiles
@ApplicationName nvarchar(256),
@ProfileAuthOptions int,
@PageIndex int,
@PageSize int,
@UserNameToMatch nvarchar(256) = NULL,
@InactiveSinceDate datetime = NULL
AS
BEGIN
DECLARE @ApplicationId uniqueidentifier
SELECT @ApplicationId = NULL
SELECT @ApplicationId = ApplicationId FROM aspnet_Applications WHERE LOWER(@ApplicationName) = LoweredApplicationName
IF (@ApplicationId IS NULL)
RETURN
-- Set the page bounds
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @TotalRecords int
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageSize - 1 + @PageLowerBound
-- Create a temp table TO store the select results
CREATE TABLE #PageIndexForUsers
(
IndexId int IDENTITY (0, 1) NOT NULL,
UserId uniqueidentifier
)
-- Insert into our temp table
INSERT INTO #PageIndexForUsers (UserId)
SELECT u.UserId
FROM dbo.aspnet_Users u, dbo.aspnet_Profile p
WHERE ApplicationId = @ApplicationId
AND u.UserId = p.UserId
AND (@InactiveSinceDate IS NULL OR LastActivityDate <= @InactiveSinceDate)
AND ( (@ProfileAuthOptions = 2)
OR (@ProfileAuthOptions = 0 AND IsAnonymous = 1)
OR (@ProfileAuthOptions = 1 AND IsAnonymous = 0)
)
AND (@UserNameToMatch IS NULL OR LoweredUserName LIKE LOWER(@UserNameToMatch))
ORDER BY UserName
SELECT u.UserName, u.IsAnonymous, u.LastActivityDate, p.LastUpdatedDate,
DATALENGTH(p.PropertyNames) + DATALENGTH(p.PropertyValuesString) + DATALENGTH(p.PropertyValuesBinary)
FROM dbo.aspnet_Users u, dbo.aspnet_Profile p, #PageIndexForUsers i
WHERE u.UserId = p.UserId AND p.UserId = i.UserId AND i.IndexId >= @PageLowerBound AND i.IndexId <= @PageUpperBound
SELECT COUNT(*)
FROM #PageIndexForUsers
DROP TABLE #PageIndexForUsers
END
Как видно, постоянно создаются временные таблицы, что просто сводит на нет любое железо. Представьте, если в секунду 100 таких вызовов будет.
Поэтому всегда создавайте свои собственные провайдеры.
ASP.NET MVC – прекрасный фреймворк для создания RESTful-приложений. Для предоставления API мы могли, например, написать такой код:
public class AjaxProductsController : Controller
{
private readonly IProductRepository productRepository;
public AjaxProductsController(IProductRepository productRepository)
{
this.productRepository = productRepository;
}
public ActionResult Details(int id)
{
return Json(productRepository.Find(id));
}
public ActionResult List(int category)
{
var products = from p in productRepository.All
where p.CategoryId == category
select p;
return Json(products.ToList());
}
}
Да, одним из выходов было написание отдельного контроллера для обслуживания AJAX-запросов.
Другим – спагетти-код:
public class ProductsController : Controller
{
private readonly IProductRepository productRepository;
public ProductsController(IProductRepository productRepository)
{
this.productRepository = productRepository;
}
public ActionResult List(int category)
{
var products = from p in productRepository.All
where p.CategoryId == category
select p;
if (Request.IsAjaxRequest())
{
return Json(products.ToList());
}
return View(products.ToList());
}
}
А если еще и необходимо добавить CRUD-операции, то:
[HttpPost]
public ActionResult Create(Product product)
{
if (ModelState.IsValid)
{
productRepository.InsertOrUpdate(product);
productRepository.Save();
return RedirectToAction("Index");
}
return View();
}
Как видно атрибуты, детектирование AJAX в коде – не самый чистый код. Мы же пишем API, верно?
Выход MVC4 ознаменовал новый функционал Web API. На первый взгляд – это смесь MVC-контроллеров и WCF Data Services.
Не буду приводить туториал на тему Web API, их много на самом сайте ASP.NET MVC [14].
Приведу лишь пример переписанного вышеприведенного кода.
Для начала чуть изменим метод InsertOrUpdate из ProductRepository:
public Product InsertOrUpdate(Product product)
{
if (product.ProductId == default(int)) {
// New entity
return context.Products.Add(product);
}
// Existing entity
context.Entry(product).State = EntityState.Modified;
return context.Entry(product).Entity;
}
И напишем сам контроллер:
public class ProductsController : ApiController
{
/*
* инициализация
*/
public IEnumerable<Product> GetAllProducts(int category)
{
var products = from p in productRepository.All
where p.CategoryId == category
select p;
return products.ToList();
}
// Not the final implementation!
public Product PostProduct(Product product)
{
var entity = productRepository.InsertOrUpdate(product);
return entity;
}
}
Итак, пара моментов, что же изменилось и как оно работает:
Чуть выше я указал, что Web API – смесь MVC и WCF Data Services. Но где это выражено? Все просто – новый API поддерживает OData! И работает по схожему принципу.
Например, для указания сортировки необходимо было указывать параметр в самом методе:
public ActionResult List(string sortOrder, int category)
{
var products = from p in productRepository.All
where p.CategoryId == category
select p;
switch (sortOrder.ToLower())
{
case "name":
products = products.OrderBy(x => x.Name);
break;
case "desc":
products = products.OrderBy(x => x.Description);
break;
}
return Json(products.ToList());
}
То сейчас необходимо лишь изменить метод GetAllProducts:
public IQueryable<Product> GetAllProducts(int category)
{
var products = from p in productRepository.All
where p.CategoryId == category
select p;
return products;
}
И в браузере, например, набрать следующее:
localhost/api/products?category=1& [15]$orderby=Name
Таким образом, мы избавились от отвлекающих моментов и можем теперь сосредоточиться на создании самого API.
Спасибо за внимание!
Автор: szKarlen
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/6530
Ссылки в тексте:
[1] Entity Framework 4.1: http://nuget.org/packages/entityframework
[2] MvcScaffolding: http://nuget.org/packages/MvcScaffolding
[3] серию статей: http://blog.stevensanderson.com/2011/01/13/scaffold-your-aspnet-mvc-3-project-with-the-mvcscaffolding-package/
[4] Ninject: http://nuget.org/packages/Ninject
[5] MVC3: http://nuget.org/packages/Ninject.MVC3
[6] NLog: http://nuget.org/packages/NLog
[7] PagedList: http://nuget.org/packages/PagedList
[8] этой: http://habrahabr.ru/company/microsoft/blog/133845/
[9] Lucene.NET: http://nuget.org/packages/Lucene.Net
[10] SimpleLucene: http://nuget.org/packages/SimpleLucene
[11] Reactive Extensions for JS: http://nuget.org/packages/RxJS-Main/1.0.10621.0
[12] поискать: http://nuget.org/packages/Backbone.js
[13] мой пост: http://habrahabr.ru/post/132463/
[14] ASP.NET MVC: http://www.asp.net/web-api/overview
[15] localhost/api/products?category=1&: http://localhost/api/products?category=1&
Нажмите здесь для печати.