Bad notes about ASP.NET MVC. Part 1 (and the only one)

    Recently, articles about ASP.NET MVC often began to appear on Habré. However, in this article I would like to make a few notes about building applications on the above framework: the minimum set of NuGet-packages (without which it’s a sin to start work), logging, pitfalls when using standard membership-, profile- providers. And finally, why the Web API from MVC 4 is what we have all been waiting for.

    Nuget-packages


    So, let’s decide without which packages you can’t start developing a web application on ASP.NET MVC. Although the list below contains those [packages] that are installed by default when creating a solution, I will still include them.
    • Entity Framework 4.1 (along with Code First) - data access
    • jQuery (UI, Validation) - [no comments]
    • Microsoft Web Helpers
    • MvcScaffolding - Code Generation
    • Ninject (MVC3) - dependency injection
    • NLog (Config, Extended, Schema) - logging
    • PagedList (MVC3) - a very convenient package for "paging"
    • Lucene (SimpleLucene) - Search
    • Reactive Extensions for JS - Client

    Entity Framework 4.1 - the question is, why is it? Well, let’s explain by example. There are a sufficient number of other similar, superior, etc. ORM frameworks (one NHibernate is worth it). A couple of years ago, I would recommend starting with the use of lightweight (relatively, judging by the synthetic tests) LINQ to SQL. BUT! The release of Entity Framework 4.1 together with Code First outweighed all the disadvantages: prototyping the application data layer was a pleasure. If for the first you need to work in a designer, deal with DBML files, then here we are only working with POCO. For example, a data model for a store:

    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 Products { get; set; }
    }
    public class ProductsContext : DbContext
    {
        public DbSet Categories { get; set; }
    }
    

    MvcScaffolding - Need to quickly sketch a CRUD panel? Already have an EF model, or LINQ to SQL? Then enter this command in the NuGet window and rejoice at code generation:
    Scaffold Controller [имя модели] –Repository
    The –Repository flag allows you to create a repository for working with the data layer at the same time.
    For example, we use the above model.
    After input, the
    Scaffold Controller Product –Repository
    following CRUD pages and abstract repository will be generated:

    public interface IProductRepository
    {
        IQueryable All { get; }
        IQueryable AllIncluding(params Expression>[] includeProperties);
        Product Find(int id);
        void InsertOrUpdate(Product product);
        void Delete(int id);
        void Save();
    }
    

    And also its implementation:

    public class ProductRepository : IProductRepository
    {
        ProductsContext context = new ProductsContext();
        public IQueryable All
        {
            get { return context.Products; }
        }
        public IQueryable AllIncluding(params Expression>[] includeProperties)
        {
            IQueryable 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();
        }
    }
    


    For a more detailed acquaintance, I advise you to read a series of articles from the creators themselves.

    Ninject - I personally do not have the opportunity to work without abstractions. ASP.NET MVC has many features for controlling / expanding the functionality of its factories. Therefore, tying a functional on specific class implementations is a bad form. Why Ninject? The answer is simple - it is lightweight, has many extensions, is actively developing.
    Install it, as well as the addition to it MVC3 :
    After that, the App_Start folder will appear, where the NinjectMVC3.cs file will be located.
    To implement DI, create a module:

    class RepoModule : NinjectModule
    {
        public override void Load()
        {
            Bind().To();
            Bind().To();
        }
    }
    


    In the NinjectMVC3.cs file in the CreateKernel method, write:

    var modules = new INinjectModule[]
                {
                    new RepoModule()
                };
                var kernel = new StandardKernel(modules);
                RegisterServices(kernel);
                return kernel;
    


    Now write our controller:

    public class ProductsController : Controller
    {
        private readonly IProductRepository productRepository;
        public ProductsController(IProductRepository productRepository)
        {
            this.productRepository = productRepository;
        }
    }
    

    NLog - how to find out how the application works, successes / failures when performing operations? The easiest solution is to use logging. Writing your bikes makes no sense. Of all, I think you can distinguish NLog and log4net. The latter is a direct port with Java (log4j). But its development is not very active, if not abandoned at all. NLog, on the contrary, is actively developing, has rich functionality and a simple API.
    How to quickly add a logger:

    public class ProductController : Controller
    {
        private static Logger log = LogManager.GetCurrentClassLogger();
        public ActionResult DoStuff()
        {
            //very important stuff
            log.Info("Everything is OK!");
            return View();
        }
    }
    

    PagedList - do you need a page turning algorithm? Yes, you can sit and come up with it yourself. But why? In this article there is a detailed description of working with him.

    Lucene.NET - Are you still erasing using the search for the database itself? Forget it! A couple of minutes and you will have an ultra-fast search.
    Install it, as well as the addition to it SimpleLucene :
    First, we automate the work with creating the index:

    public class ProductIndexDefinition : IIndexDefinition
    {
        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());
        }
    }
    


    As you can see in the Convert method, we serialize POCO in the Lucene Document.
    Controller Code:

    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, 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();
    		}
    }
    


    To display the results, create a ResultDefinition:

    public class ProductResultDefinition : IResultDefinition
    {
        public Product Convert(Document document)
        {
            var product = new Product();
            product.ProductId = document.GetValue("ProductId");
            product.Name = document.GetValue("Name");
            product.Price = document.GetValue("Price");
            product.CategoryId = document.GetValue("CategoryId");
            product.CreationDate = DateTools.StringToDate(document.GetValue("CreationDate"));
            product.Description = document.GetValue("Description");
            return product;
        }
    }
    


    This is where POCO deserializes.
    And finally, we automate the work with requests:

    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;
            }
        }
    }
    


    Now let's move on to the controller:

    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(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 - should be the basis of the client. No, honestly, a smoother creation of the application framework on the client with the possibility of unit testing should still be sought . I advise you to read my post on Rx development.

    Authentication and Authorization


    I warn you right away - never use the standard AspNetMembershipProvider! If you look at his monstrous stored procedures out of the box, you just want to throw him out.
    Open the InstallMembership.sql and InstallProfile.SQL files in the C: \ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ folder.
    For example, this is what the SQL code for FindUsersByName from InstallMembership.sql looks like:

    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
    


    And here is Profile_GetProfiles from 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
    


    As you can see, temporary tables are constantly being created, which simply negates any hardware. Imagine if there are 100 such calls per second.
    Therefore, always create your own providers.

    ASP.NET MVC4 Web API


    ASP.NET MVC is a great framework for creating RESTful applications. To provide the API, we could, for example, write the following code:

    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());
        }
    }
    


    Yes, one way out was to write a separate controller to serve AJAX requests.
    The other is the spaghetti code:

    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());
        }
    }
    


    And if you also need to add CRUD operations, then:

    [HttpPost]
    public ActionResult Create(Product product)
    {
        if (ModelState.IsValid)
        {
            productRepository.InsertOrUpdate(product);
            productRepository.Save();
            return RedirectToAction("Index");
        }
        return View();
    }
    

    As you can see the attributes, AJAX detection in the code is not the cleanest code. We are writing an API, right?
    The release of MVC4 marked the new functionality of the Web API. At first glance, this is a mixture of MVC controllers and WCF Data Services.
    I will not give a tutorial on the Web API topic, there are many of them on the ASP.NET MVC site itself .
    I will give only an example of the rewritten code above.
    First, let's change the InsertOrUpdate method from 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;
    }
    


    And write the controller itself:

    public class ProductsController : ApiController
    {
        /*
         * инициализация
         */
        public IEnumerable 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;
        }
    }
    


    So, a couple of points, what has changed and how it works:
    • Now controllers inherit from ApiController
    • No more ActionResult, etc. - only clean code
    • No more HttpPost, etc. attributes
    • The method name should begin with Get for get requests, POST for post requests.
    • The analogue of the Index method in the Web API is GetAll {0} - the name of the controller

    I pointed out a little higher that the Web API is a mixture of MVC and WCF Data Services. But where is this expressed? It's simple - the new API supports OData! And it works on a similar principle.
    For example, to specify sorting, it was necessary to specify a parameter in the method itself:

    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());
    }
    


    Now you just need to change the GetAllProducts method:

    public IQueryable GetAllProducts(int category)
    {
        var products = from p in productRepository.All
                       where p.CategoryId == category
                       select p;
        return products;
    }
    


    And in the browser, for example, type the following:
    http://localhost/api/products?category=1&$orderby=Name

    Thus, we got rid of distractions and can now focus on creating the API itself.

    Thanks for attention!

    Also popular now: