Пользователь
8,0
рейтинг
29 апреля 2012 в 16:16

Разработка → Непутевые заметки о ASP.NET MVC. Часть 1 (и единственная)

В последнее время на Хабре часто начали появляться статьи о ASP.NET MVC. Однако в этой статье я бы хотел сделать несколько заметок по поводу построения приложений на вышеприведенном фреймворке: минимальный набор NuGet-packages (без которых грех начинать работу), логирование, подводные камни при использовании стандартных membership-, profile- провайдеров. И, напоследок, почему Web API из MVC 4 — то, что так долго мы все ждали.

NuGet-packages


Итак, определимся без каких пакетов нельзя начинать разрабатывать веб-приложение на ASP.NET MVC. В нижеприведенном списке хоть и находятся те [пакеты], которые по-умолчанию ставятся при создании решения, но я их все же включу.
  • Entity Framework 4.1 (вместе с Code First) — доступ к данным
  • jQuery (UI, Validation) — [no comments]
  • Microsoft Web Helpers
  • MvcScaffolding — кодогенерация
  • Ninject (MVC3) — dependency injection
  • NLog (Config, Extended, Schema) — логирование
  • PagedList (MVC3) — очень удобный пакет для «листания страниц»
  • Lucene (SimpleLucene) — поиск
  • Reactive Extensions for JS — клиент

Entity Framework 4.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 — нужно быстро набросать 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();
    }
}


Для более детального ознакомления советую почитать серию статей от самих создателей.

Ninject — лично мне не представляется возможность работать без абстракций. ASP.NET MVC имеет множество возможностей контроля/расширения функционала своих фабрик. Поэтому завязывание функционала на конкретных реализациях классов — плохой тон. Почему Ninject? Ответ прост — он легковесен, имеет множество расширений, активно развивается.
Установим его, а также дополнение к нему MVC3:
После этого появится папка 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 — как узнать, как работает приложение, успехи/неудачи при выполнении операций? Самое простое решение — использовать логирование. Писать свои велосипеды смысла нет. Из всех, думаю, можно выделить 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 — нужен алгоритм «листания страниц»? Да можно самому посидеть и придумать. Но зачем? В этой статье есть детальное описание работы с ним.

Lucene.NET — Вы все еще стираете используете поиск самой БД? Забудьте! Пара минут и у Вас появится сверхскоростной поиск.
Установим его, а также дополнение к нему SimpleLucene:
Первым делом автоматизируем работу с созданием индекса:

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 — должен быть основой клиента. Нет, честно, более плавного создания каркаса приложения на клиенте с возможностью юнит-тестирования надо еще поискать. Советую почитать мой пост по разработке на Rx.

Аутентификация и авторизация


Сразу же предупреждаю – никогда не используйте стандартный AspNetMembershipProvider! Если посмотреть на его монструозные хранимые процедуры из коробки, то просто захочется его выкинуть.
Откройте в папке C:\Windows\Microsoft.NET\Framework\v4.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 MVC4 Web API


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.
Приведу лишь пример переписанного вышеприведенного кода.
Для начала чуть изменим метод 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;
    }
}


Итак, пара моментов, что же изменилось и как оно работает:
  • Теперь контроллеры наследуются от ApiController
  • Больше никаких ActionResult и т.п. – только чистый код
  • Больше никаких HttpPost и т.п. атрибутов
  • Имя метода должно начинаться с Get для get-запросов, POST – для post-запросов.
  • Аналог метода Index в Web API – GetAll{0} – имя контроллера

Чуть выше я указал, что 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;
}


И в браузере, например, набрать следующее:
http://localhost/api/products?category=1&$orderby=Name

Таким образом, мы избавились от отвлекающих моментов и можем теперь сосредоточиться на создании самого API.

Спасибо за внимание!
Карлен Симонян @szKarlen
карма
96,0
рейтинг 8,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (72)

  • +6
    Хороший обзор.
    Сам единственно заменил бы Ninject на Autofac.
    И, возможно, добавил KnockoutJS.
    • +1
      спасибо!
    • 0
      KnockoutJS входит в состав ASP.NET MVC 4 Beta
    • +1
      А если сравнивать с Unity, что лучше?
      • 0
        Ничего, они «все одинаковые» в этом плане. То есть, все способны делать все, что полагается современному контейнеру.
      • 0
        Если говорить о наборе возможностей, то как уже сказали, современные контейнеры обладают примерно одинаковым набором возможностей. Это раньше были только PicoContainer да StructureMap, а сейчас контейнеров много.
        Unity в первых версиях отличался очень громоздким синтаксисом, как сейчас — не знаю.
        Autofac всегда старался избегать Reflection'а, одним из первых стал использовать такие фичи C#, как Generics, lambda expressions, что и сделало его одним из самых быстрых контейнеров. Есть интеграция с WCF, ASP.NET, ASP.NET MVC, NMock, DynamicProxy, Moq.
        Хорошая документация и подробные статьи по различным аспектам использования в блоге автора.
        Часто обновляется, автор работает в Microsoft над проектом MEF.
    • 0
      Сам юзаю StructureMap и AutoFac, но Ninject — вполне достоен. Тут уже дело вкуса.
    • 0
      Это уже сугубо личные предпочтения.
      Я бы использовал Castle.Windsor для контейнеров =) Так уж сложилось исторически
      • 0
        Да, согласен, чаще всего это вопрос личных предпочтений. Мне Autofac нравится, помимо прочего, лаконичным синтаксисом, но это дело вкуса.

        Не раз встречал сравнения производительности, где Ninject не блистал. Вот например:
        philipm.at/2011/0808/
        Статья прошлогодняя, возможно у Ninject'f что-то успело поменяться к лучшему в плане производительности.
    • 0
      Ага, судя по данной статье, Ninjet и Unity вообще черепахи по сравнению с Autofac.
  • +5
    Немного истории: Глен Блок, автор замечательной библиотеки контейнеров MEF ушел работать в команду WCF, где начал делать упрощенный REST API в рамках WCF, эта работа получила название WCF Web API. Потребность в упрощенном способе создания сервисов поверх HTTP была давно.

    Набор получился настолько хорошим, что его решили включить в саму базовую систему ASP.NET. Это включение и получило название ASP.NET WebAPI. Таким образом WebAPI — это часть ASP.NET, базовый компонент. Поэтому, например, WebAPI можно использовать в проектах ASP.NET WebForms.
  • +2
    Достойный набор.
    • 0
      спасибо!
  • 0
    Хочется обратить внимание на то, что уже вышел Entity Framework 4.3. В нем появилось пару приятных плюшек, миграции, например.
    • 0
      Миграция была ещё и в 4.1, только срая и качалась отдельно, из возможностей было только генерация SQL скрипта для внесения изменений в базу.
  • +2
    У Entity Framework есть один существенный минус, который для кого-то перекрывает все плюсы — он не поддерживается в Mono, во всяком случае пока.
    Ну и соответственно другие от него зависимые packages также имеют такой минус.

    А так — спасибо за подбор, буду иметь ввиду. Правда хотелось бы еще и исходников для не слишком продвинутых и ленивых людей вроде меня.
  • 0
    После Scaffold на Product и Category и переходу на представление создания продукта получаем ошибку:

    «Value cannot be null. Parameter name: source» на строке:

    @Html.DropDownListFor(model => model.CategoryId, ((IEnumerable<MvcPackTest.Models.Category>)ViewBag.PossibleCategories).Select(option => new SelectListItem {
            Text = (option == null ? "None" : option.Name), 
            Value = option.CategoryId.ToString(),
            Selected = (Model != null) && (option.CategoryId == Model.CategoryId)
        }), "Choose...")
    


    :(
    • 0
      В контроллере имеем:

              public ActionResult Create()
              {
      	    ViewBag.PossibleCategory = ...;
                  return View();
              } 
      


      а в представлении:

      @Html.DropDownListFor(...ViewBag.PossibleCategories).Select(...
      


      Печально.
    • 0
      честно говоря, у меня все работает.
      как вариант, попробуйте сначала заполнить хотя бы категории.

      однако возможен и баг )
      • 0
        Я уже написал выше что для контроллера свойство ViewBag генерируется в ед. числе, а для представления по мн… У вас это не так?
        • 0
          все понял — я подумал, что не происходит запросов, и данные = null.
          насчет ед. и мн. числа — да это самый настоящий баг. почему так происходит? не знаю. смотрел исходники T4-шаблонов, но везде используется плюрализация имен.
          возможно баг скрыт где в недрах T4-скаффолдера.
          как решение, Ctrl+H :)
  • +1
    С WebAPI, конечно, приехали наконец. Мы уже 3 года как написали свой неизменный ActionInvoker, который позволяет возвращать из контроллера модель вместо ActionResult — а это самое ценное свойство ApiController'а. Конечно, всякие вкусности, вроде «IQueryable через адресную строку» надо будет еще распробовать.
    • 0
      blog.ploeh.dk/2012/03/26/IQueryableIsTightCoupling.aspx IQueryable очень нехорошо выставлять наружу. SQL сгенерированный по произвольным запросам содержит обычно такое количество join и другого непотребства что самое крутое железо уходит на секунды.
  • +1
    Кстати, про будущие нововведения в WebAPI можно узнавать в этом блоге:

    blogs.msdn.com/b/henrikn/archive/2012/04/23/using-cookies-with-asp-net-web-api.aspx

    и еще запись

    blogs.msdn.com/b/henrikn/archive/2012/04/27/asp-net-web-api-updates-april-27.aspx
  • +3
    "
    То сейчас необходимо лишь изменить метод GetAllProducts:

    public IQueryable GetAllProducts(int category)
    {
    var products = from p in productRepository.All
    where p.CategoryId == category
    select p;
    return products;
    }
    И в браузере, например, набрать следующее:
    localhost/api/products?category=1&$orderby=Name
    "

    Ну да, конечно. Очередная серебряная пуля. Теперь задумываемся внимательно.

    Любое соединение с БД — это… соединение. Оно может быть инкапсулировано в контекст, а может быть в него не инкапсулировано, но его надо и открыть, и закрыть. Смотрим в реализацию репозитория — и видим там, конечно же, этот самый контекст (ProductsContext context = new ProductsContext();), причем, «естественно», без детерминированного закрытия (что означает, что соединение будет закрыто тогда, когда этого захочется контексту, то есть, в худшем случае, тогда, когда до этого класса доберется GC). В контроллере, понятное дело, повтор того же сценария — репозиторий открывается при создании контроллера и не имеет явного закрытия.

    И это, на самом деле, не от лени создателей, и не от того, что они не знают, как работать с IDisposable, а потому, что если реализовать в репозитории принудительное закрытие, то вся эта красота с «вернем из контроллера IQueryable, а потом сделаем с ним все операции» работать не то что бы перестанет, но по крайней мере не будет выполнять эти операции на уровне базы.

    Что в сухом остатке? В сухом остатке — надо очень внимательно следить за тем, как ведут себя соединения в подобных (deferred execution) сценариях. Да, утверждается, что EF открывает соединения по месту и закрывает их сразу после завершения работы запроса. Окей, прекрасно. Главное, чтобы у вас за репозиторием оказался именно EF, который так умеет, и чтобы его понимание «завершения работы» совпало с вашим.
    • 0
      ИМХО ADO.NET уже настолько умен и прозрачен, что можно забыть про соединения.
      По факту большая часть открытия новых соединений, это вызовы в контексте уже открытого соединения.
      • +2
        «ИМХО ADO.NET уже настолько умен и прозрачен, что можно забыть про соединения.»
        Вообще-то, нет. Как нужно было аккуратное управление, так и нужно. Не далее как в прошлом году получили падение приложения из-за того, что программист забыл закрыть соединение в часто используемом запросе. *Два* тестировщика положили приложение.

        «По факту большая часть открытия новых соединений, это вызовы в контексте уже открытого соединения.»
        Вы путаете. Connection pool (а вы говорите о нем) экономит ресурсы на создание/уничтожение соединений, а не на их открытие/закрытие. Открытое соединение — это реально занятые ресурсы сервера БД, и их все стараются экономить, и держать соединение открытым как можно меньше.
        • 0
          Ты прав. Сам сталкивался с этим. Еще была проблема, что изменяешь данные, а так как Dispose не вызывается, то реально данные остались старые и в другом контексте они старые. Чтобы избежать открытых соединений, нужно интерфейс репозитория наследовать от IDisposable и организовать работу DI Container'ов таким образом, чтобы на каждый HttpContext создавался свой репозиторий, а в конце вызывался Dispose. Посмотреть как сделано с Unity, например, можно в расширении Unity.MVC.
          • 0
            «Еще была проблема, что изменяешь данные, а так как Dispose не вызывается, то реально данные остались старые и в другом контексте они старые.»
            ??! Вообще-то, вызов SaveChanges гарантирует, что данные ушли в БД. Вот тут-то Dispose не при чем.

            «Чтобы избежать открытых соединений, нужно интерфейс репозитория наследовать от IDisposable и организовать работу DI Container'ов таким образом, чтобы на каждый HttpContext создавался свой репозиторий, а в конце вызывался Dispose.»
            Скажем так, это _один_ из вариантов.

            Я в основном предпочитаю детерминированное управление жизненным циклом, с явным using. Это намного очевиднее для разработчика, и работает (практически) во всех случаях — кроме вот такого вот deferred execution, как предлагает Web API.
            • 0
              Даже после SaveChanges данные не обновлялись. Проблема решилась сменой LifetimeManager'а на HierarchicalLifetimeManager и использовании Child DI Container'а для каждого HttpContext'а (суть Unity.MVC). При этом контейнер вместе с созданными объектами правильно Dispose'ился.

              А явный using вызывается, например, в MembershipProvider'е, потому что он создается 1 на Application и там схема описанная выше не работает.
              • +1
                «Даже после SaveChanges данные не обновлялись.»
                Это очень странно. Dispose отпускает ресурсы, а не коммитит изменения.

                Я подозреваю, что ваша проблема была не в том, что контекст не отпускался, а в том, что вы брали контекст, который уже содержал данные, и они успевали устареть.
    • 0
      blog.ploeh.dk/2012/03/26/IQueryableIsTightCoupling.aspx
      Проблемы не только с «висячим» контекстами. А также с неоптимальными запросами, в EF LINQ бывает достаточно поменять порядок чтобы запрос сократился в разы. По соображениям производительности нам сегодня нужно денормализировать некоторые таблицы, при подходе с IQueryable вы зацепите всех клиентов.

      • 0
        «По соображениям производительности нам сегодня нужно денормализировать некоторые таблицы, при подходе с IQueryable вы зацепите всех клиентов.»
        Вот этот пассаж мне совершенно не очевиден. Хотя бы потому, что он оперирует разными уровнями абстракции: таблицы — это БД, а IQueryable в данном контексте — внешний публичный API, т.е. на три уровня абстракции дальше (запрос поверх БД, провайдер поверх запроса, API поверх провайдера).

        А вообще, мы имеем дело с классической архитектурной дилеммой — гибкость, удобство и универсальность с одной стороны, и производительность — с другой. Ничего нового, в общем-то.
        • 0
          msdn.microsoft.com/en-us/library/system.linq.iqueryable.aspx
          — Provider — это такая ох… тельно большая деталь реализации. Я то знаю что MySQL .NET Connector не имеет оператора IN и генерирует код похуже чем Майкрософтовский для MS SQL, но разкажите ли вы об этом все своим клиентам, и сколько из них решат не заморачиватся и положат сервер JOIN-ом на 16 таблиц?
          — Expression — учитывает взаимоотношения, IEnumerable это более слабая связь.
          — Из соображений производительности часть данных мы выносим в Dynamo DB, клиенты об этом ни слухом ни духом и это правильно.
          В сухом остатке: Uncle Bob Martin определяет хорошую архитектуру, как такую которая позволяет откладывать принятие важных решений до момента когда это будет действительно нужно. Наш подход позволил сменить: MS SQL на MySQL, MySQL частично заменить Dynamo DB, на следующем этапе ещё часть иерархичных структур уедет в Ellastic Search и клиентов это до поры времени никак не затрагивает. Что было бы с IQueryable да тоже самое только потеряли время на отвязывание IQueryable.
          • 0
            «Provider — это такая ох… тельно большая деталь реализации.»
            Я в курсе. Я их писал.

            «Я то знаю что MySQL .NET Connector не имеет оператора IN и генерирует код похуже чем Майкрософтовский для MS SQL, но разкажите ли вы об этом все своим клиентам, и сколько из них решат не заморачиватся и положат сервер JOIN-ом на 16 таблиц?»
            Простите, «мои клиенты» — это люди. И их никто не пустит писать join-ы в сервере.

            «Наш подход позволил сменить [...] Что было бы с IQueryable да тоже самое только потеряли время на отвязывание IQueryable. „
            Угу. А запросы произвольной сложности “ваш подход» тоже позволяет строить?

            В общем, шило на мыло. Кому нужно чистую производительность — тем, понятное дело, и EF не к месту, и IQueryable не очень с руки. Но не всем она нужна, некоторым важнее другие, уже описанные выше аспекты.
    • 0
      Это хорошая лазейка для говнокодеров :).
      Не имея знаний и опыта можно такого будет понаписать, что у многих потом волосы дыбом вставать будут.
      Для тех кто понимает это довольно интересная возможность.

      Естественно разработчики столь интересной возможности подумали над открытием и закрытием контекста.
      Для работы с базой данных нужно пользоваться немного другим контроллером (производные от System.Web.Http.Data.DataController) и тогда все встает на свои места.

      Возврат IQueryable без использования System.Web.Http.Data.DataController контроллеров подходит для не больших коллекций уже выбранных объектов, это нужно знать и учитывать при разработке.
      • 0
        «Это хорошая лазейка для говнокодеров :).»
        Такие вещи называются «способ выстрелить себе в ногу». Чем их в системе меньше (как и любого недетерминированного поведения), тем лучше.

        «Для работы с базой данных нужно пользоваться немного другим контроллером (производные от System.Web.Http.Data.DataController) и тогда все встает на свои места.»
        Ага. И убить тем самым всю идею паттерна Repository, описанного в статье. Нет, я не против, но только уж либо одно, либо другое.
        • 0
          Web Api на стадии беты и к пререлизу может многое измениться.
          Сейчас можно написать свой класс производный от DataController для управления контекстом базы данных (например connection scope) и пользоваться своими репозиториями как и раньше
          • 0
            Вот только в посте об этом — ни слова. Зато рассказывают про ApiController.
            • 0
              DataController наследуется от ApiController и реализует IDisposable.

              Если его не использовать, то можно обойтись следующим: для управления жизненнным циклом DbContext, можно реализовать IDisposable в самом репозитории, а в RepoModule прописать:

              Bind().To().InRequestScope;

              Ninject проконтролирует, чтобы в конце запроса был вызван Dispose().
              • 0
                Угу, спасибо.

                Я, собственно, об этом и говорил: легкость «тут добавили репозиторий поверх EF, а тут — ApiController поверх репозитория» — она видимая, и в реальности неплохо бы учитывать тонкости тут, там и там.
  • +2
    Годная статья — узнал тренды. Но…

    1. EF — не люблю ормы использую максимум мапер, но на это тему я уже холиварил…

    2. Scaffolding — бред. Еще ни разу такое решение не работало. Всегда надо что то кастомное. Сейчас рулят бутстрапы — по сути готовая верстка, знай себе клепай вьюхи да процедуры. В случае написания админки просто идеально. Например twitter.github.com/bootstrap/index.html

    3. Ninject. Я использую unity, но с ходу не нашел существенных отличий.

    4. NLog. Высоко нагруженный сайт — хороший повод написать велосипед:) Логирование должно минимально влиять на производительность. Т.к. на прошлом проекте средней нагрузки log4net стал причиной тормозов, я не пожалел пары дней и написал свое логирование с шахматами и монашками.
    Хотя NLog обязательно испытаю на проекте с меньшей нагрузкой.

    5. PagedList — да о такой штуке сразу думаешь когда переходишь с WebForms, но по сути саму идею пейджинга для таблиц пора выкинуть на помойку. Добротный поиск — вот, что нужно пользователю.

    6. Lucene.NET — интересная штука, надо посмотреть. Но вряд ли бы я отважился добавить это в реальный проект. И уж совсем не понятно как это может сравнится с мощью sql сервера.

    7. На счет AspNetMembershipProvider это вы зря — нормальное решение для малых и средних проектов. Да процедуры написаны под sql 2000 потому и выглядят как дерьмо мамонта. Но процедуры из вашего примера используются лишь для редактирования пользователей. А вот для авторизации как раз все неплохо оптимизировано.

    8. ASP.NET MVC4 Web API — да похоже ребята опять что то крутое готовят.
    • 0
      1. EF — больше, чем ORM. Как минимум Code First, оценил и полюбил эту штуку с первым же проектом, рекомендую!
      2. Согласен. Генераторы на помойку. Хотя для быстрого создания прототипа, самое то.
      3. Тоже самое.
      4. NLog проявил себя неплохо. Но! Всё зависит от того, как настроен логгер и как он используется. Возможностей прострелить себе производительность масса.
      5. Соглашусь. Пейджинг — олдскул и моветон. Кто лезет дальше 2-3 и последней страницы? :)
      6. Чего так? Как раз вот думаю.
      7. Мы честно пытались, не пригодился ни разу. Каждый проект со своими плюшками и пряниками. Только интерфейсы.
      8. Мне честно говоря уже даже немного страшно за будущее ASP.NET MVC. Простота настораживает.
    • 0
      6. Lucene.NET — интересная штука, надо посмотреть. Но вряд ли бы я отважился добавить это в реальный проект. И уж совсем не понятно как это может сравнится с мощью sql сервера.

      почему бы нет?
      все данные для их целостности хранить в БД. параллельно можно интересующие поля также хранить и в Lucene, который по синтетическим тестам чуть ли не в 20 раз быстрее Full-Text Search'a, например, MS SQL.
      кстати, Lucene также использует twitter.
      • 0
        Даже если поверить в тесты, что важнее в 20 раз быстрее или в 2 раза больше гемора при поддержке? Я ведь не пишу твиттер. Короче если и использовать Lucene то далеко не в каждом проекте.
        • 0
          Конечно, не в каждом — а там, где это нужно. У SQL Server-а, кстати, есть фича полнотекстового поиска.
          • 0
            спасибо кэп
    • +1
      Вот что пишут про Lucene (ну а Lucene.NET--его порт): Since we love Open Source here at Twitter we chose Lucene, a search engine library written in Java, as a starting point. После этого вы будете сомневаться, брать ли его на реальный проект?!
      • +2
        А также LinkedIn и многие другие.
    • +1
      1 и 2е у вас не вяжется. EF окромя того что ОРМ позволяет очень быстро прототипировать сайты, админки и всё прочее. Автоматические миграции вообще сказка. Скаффолдинг просто позволяет уменьшить количество нажатий. Собтсно то самое зачем Twitter Bootstrap юзают
      6. Lucene.net можно сказать ядро RavenDb и с перформансом у них просто отлично.
      • 0
        Разница в том, что бутсрап это код который я не в коем случае не буду редактировать. Мне нужно просто прочитать инструкцию и использовать. А скаффолдинг это куча бесполезного кода который придется менять. Зачем? Я быстрее и качественней с нуля напишу.
        • 0
          Не напишете. Ведь вы даже не смотрите, как что написано, а пишете своё. Так, например, в скафолдинге можно менять T4 темплейты.

          Но ведь что такое T4 вы не знаете, так как «зачем это нужно?». Вы раскритиковали несколько технологий, которые используются в промышленных масштабах. Такое позволительно лишь студентам, которые с интезиазмом берутся за написание пусть и кривого, но своего.

          Максимум, чего от вас, как от человека в данной ситуации можно ожидать, так это то, что вы мельком прочтёте что такое T4, да какой-нибудь более детальный обзор о скафолдинге. Слов эдак на 300—400 и ввяжетесь в очередной спор «кастомное против готового».

          Я искренне желаю вам не терять этой вашей энергии, но тратить её разумно.
          • 0
            Зачем мне менять T4 темплейты, если я могу просто писать код?

            Знаю. Использование технологии в промышленных масштабах, не означает, что она лишена недостатков.

            Кстати, действительно, не подумал об использовании скаффолдинга в качестве инструмента для обучения.
            Вот только матерому профи не нужен весь этот нагенеренный мусор. Профи знает что, как и где написать, чтобы все было красиво, оптимально и без боли.

            Да, я адепт кастомного и добротных компонентов. В кодогенерации разочаровался еще в студенчестве. Полуфабрикаты не люблю, когда использую подобное никак не могу отделаться от ощущения, что если бы написал все сам получилось бы все проще, быстрее и надежнее.
    • 0
      мощью sql сервера. — По сравнению с Lucene full-text search MS SQL убогий.
      • 0
        Ну я имел ввиду sql сервер как цельное решение. Может и убогий, но у нас неплохо справляется, а затрат на поддержку добавилось 0.
        • 0
          Full-text search + MS SQL для хорошей производительности требуют DBA, так что затраты на поддержку отличны от 0. Даже если DBA у вас нет то кто-то у вас периодически этим занимается и его время тоже стоит денег. И процедуры резервного копирования и железо на которое сбрасываются резервные копии тоже денег стоят. По сравнению с этим Lucene стоит копейки. Это не значит что стоит лепить Lucene всюду где можно и где нельзя, но когда я слышу что MS SQL это дёшово, мне сложно поверить. Дороже MS SQL разве только решения от Oracle и IBM.
  • 0
    Вопрос насчет CodeFirst: допустим он сгенерировал DB по моим классам, все работает. Потом я записал в эту DB реальные данные, а потом вдруг понял что мне нужно еще одно поле в таблице или нужно поменять его тип. Насколько я знаю, EF CodeFirst не умеет такие ситуации решать — прийдется пересоздавать таблицу по-новой, теряя данные. Получается что на реальных проектах EF CodeFirst не помогает а наоборот мешает работать, так как там такие проблемы (изменение структуры DB) возникают чуть ли не каждый день.

    Теперь собственно вопрос — поменялось ли в этом плане что-нибудь в последних версиях EF?
    • 0
      хорошая новость — да, появились! начиная с версии 4.3. можно контролировать добавление/удаление/ изменение столбцов таблиц.
      хороший пост об этом.
    • +1
      «Теперь собственно вопрос — поменялось ли в этом плане что-нибудь в последних версиях EF? „
      Во-первых, поменялось. Появились миграции, которые позволяют менять структуру более мягко и контролируемо.

      Во-вторых, и это более важно, CodeFirst не обязательно подразумевает управление структурой БД из кода. У нас вот доступ сделан через CodeFirst, потому что это удобно и прозрачно, но при этом все миграции/DbInitializerStrategy отключены вообще, а управление структурой БД идет вручную по отдельному процессу.
  • 0
    я не понимаю, зачем эта холиварщина, да еще поданная под таким соусом:
    «Итак, определимся без каких пакетов нельзя начинать разрабатывать веб-приложение на ASP.NET MVC. „
    Вы мне запрещаете? ок.
    Почему не написать просто, вам нужен ORM (нужен?), log, ioc.
    Code First может и ничего, но например изменение существующей схемы базы данных в последней виданной мной версии CF вызывал жуткую попуболь. В любом случае, в nHibernate есть Automapping, который ничем не хуже, а пожалуй даже и поудобнее Code First. Не говорю что nHiberante 100% лучше чем EF, но каждому свое, кому-то и linq2Sql за глаза хватит. Но думаю ваш авторитет вряд ли позволяет вам так однозначно утверждать что нужно пользоваться только EF.
    То же самое с ninject (вам уже привели пяток ничем не худших альтернатив). К тому же не в любом и не в каждом web проекте ioc действительно необходим, незачем усложнять код без причин.
    Почему Nlog а не log4net, даже не спрашиваю)
    Ну и где тогда у вас например moq или rhinomocks — контроллеры не бум тестировать?
    • 0
      >>я не понимаю, зачем эта холиварщина, да еще поданная под таким соусом

      честно, и намека нет. не делал даже сравнения с др. технологиями (java и пр.)

      >>Вы мне запрещаете? ок.
      >Почему не написать просто, вам нужен ORM (нужен?), log, ioc.

      да Вы что? у меня НЕТ ПРАВА кому-либо, что-либо запрещать. можете начинать проект хоть с empty solution.

      >>>Но думаю ваш авторитет вряд ли позволяет вам так однозначно утверждать что нужно пользоваться только EF.
      я даже не заикнулся об этом. все же я НЕ являюсь истиной в последней инстанции. вся статья — это заметки с действующим кодом, с примерами. если Вы уже опытны, имеете свой арсенал любимых инструментов, то смело можете пропустить многие пункты. ибо на вкус и цвет...)

      и да, я указал сразу же, что EF больше подходит именно для
      >>>прототипирование слоя данных приложений стало одним удовольствием

    • 0
      забыл добавить:
      >>>Ну и где тогда у вас например moq или rhinomocks — контроллеры не бум тестировать?

      думал включать или нет тему тестирования. но для этого по объему нужна такая же статья. возможно напишу в следующей статье.

      p.s.
      и да, я адепт покрытия юнит-тестами большинства функционала.
  • 0
    >> Lucene (SimpleLucene) — поиск

    Хорошее решение для поиска. Но не рекомендовал бы его «по умолчанию». Всё же не каждому решению нужен поиск и не всегда можно представить данные в виде одной денормилизированной таблицы для индекса Lucene. Часто встречаемая ситуация, когда есть связь данных many to many, особенно если A many to many B и B many to many С. А поиск нужен по полям всех трёх. Количество записей в индексе становится заоблачным, но всё же работает быстрее SQL запроса. Вот только памяти надо море и само индексирование рискует затянуться.
    • 0
      да, полностью хранить все в Lucene не стоит, ибо это не реляционная модель данных. основной упор именно полнотекстовый, либо недетерминированный поиск.
  • 0
    Интересно. Спасибо.

    Возникло несколько вопросов, вы уж меня простите, если они покажутся глупыми. «Я ведь сварщик-то не настоящий, маску на стройке нашел...» ASP.NET MVC использую только для создания прототипов и всяких маленьких проектов, зачастую не сильно вдаваясь в подробности технологии, т.к. для меня важнее сделать быстро, по возможности приложив минимум усилий.

    1. В примере про Entity Framework в контекте объявлен DbSet только для Category. Почему? Я бы в этом случае добавил бы DbSet и для Product. Это просто лишнее или в корне не верно?

    2. MvcScaffolding для CRUD. Использую мастер AddController c соответсвующими опциями. В чем разница по сравнению с MvcScaffolding кроме командной строки / визуального мастера?

    3. По поводу стандартного AspNetMembershipProvider. Вы не слишком категоричны? Повторюсь, что использую MVC только для создания прототипов и маленьких проектов. Для себя вижу проблемы только в профилях ползователей (дополнительная информация о пользователе). На данный момень использую стандартный Membership провайдер и свои профили, связывая их по username. Может быть подскажете какое-нибудь готовое решение?
    • 0
      1. Это не лишнее, нет. если необходим доступ ко всем продуктам, минуя слой категорий, то добавляйте новый DbSet. дело вкуса и необходимости.
      2. дело в автоматизации кодогенерации (плюс меньше кликов).
      3. AspNetMembershipProvider для именно маленьких проектов подойдет. готового решения не скажу, ибо не встречал.
      совет — при создании запросов с стандартным AspNetMembershipProvider, делайте join по LoweredUserName, т.к. LoweredUserName находится в кластерном индексе для таблицы aspnet_users, а LoweredEmail в таблице aspnet_membership также в кластерном индексе.
      • 0
        Спасибо.

        2. Но ведь мастер также все автоматически создает — и контроллер со всем необходимым, и представления. Или дело только в количестве кликов? Просто пытаюсь понять — надо оно мне, или и так хорошо.
        • 0
          хорошо, мастер создаст представления и контроллер, но без репозитория заодно, например.
          а если изменилась модель? флаг -Force и скаффолдер пересоздаст все опять.
          добавили POCO — и автоматически добавиться DbSet в DbContext + все остальное.
          такого точно в мастере нет.
          • 0
            Вот теперь разобрался. Спасибо.
  • 0
    Вот далековат я от .NET, поэтому иногда возникает очень странное ощущение. Когда кто-то из дотнетчиков показывает, что, мол, раньше, смотрите, какая прорва кода нужна была, чтобы работало то-то и то-то. Смотришь — и правда простыня кода огромная. А теперь мол, смотрите, как все легко и просто. Смотришь — а там кода, конечно, меньше, но ведь все-равно ж дохрена!

    И вот не знаешь, как реагировать: то ли радоваться, то ли печалиться. Видимо, нет предела совершенствованию — всегда все можно упростить еще.

    (Относится не только к .NET)
    • 0
      лично я не встречал еще технологии, где бы вызов метода DoMagic() исполнил бы все желания, а Вы?)
  • 0
    Правильно подсказывают knockoutjs
    Также еще можно ограниченно рекомендовать mongodb — mongodb — C# — knockoutjs очень хорошо сочетаются.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.