📜 ⬆️ ⬇️

Unsuccessful notes on ASP.NET MVC. Part 1 (and only)

Recently on Habré often began to appear articles on ASP.NET MVC. 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 is 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 cannot start developing a web application on ASP.NET MVC. In the list below, although there are those [packages] that are set by default when creating a solution, but I will still include them.

Entity Framework 4.1 - the question arises, why is it? Well, I will explain with an example. There is a sufficient number of other similar, superior, and so on. ORM frameworks (one NHibernate is worth something). A couple of years ago, I would recommend to start using lightweight (relatively, judging by synthetic tests) LINQ to SQL. BUT! The release of Entity Framework 4.1 together with Code First outweighed all the disadvantages: prototyping a layer of application data was one 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, the data model for the 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<Product> Products { get; set; } } public class ProductsContext : DbContext { public DbSet<Category> Categories { get; set; } } 

MvcScaffolding - need to quickly jot down a CRUD panel? Already have a model EF, or LINQ to SQL? Then enter this command in the NuGet window and rejoice in 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, use the above model.
After entering
Scaffold Controller Product –Repository
the following CRUD pages and abstract repository will be generated:

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

And also its implementation:
')
 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(); } } 


For more detailed information 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 to control / expand the functionality of its factories. Therefore, setting the 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 MVC3 addition to it:
After this, the folder App_Start will appear, where the file NinjectMVC3.cs will be located.
To implement DI, create a module:

 class RepoModule : NinjectModule { public override void Load() { Bind<ICategoryRepository>().To<CategoryRepository>(); Bind<IProductRepository>().To<ProductRepository>(); } } 


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

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


Now we will write our controller:

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

NLog - how to find out how an application works, success / failure when performing operations? The simplest solution is to use logging. It makes no sense to write your bikes. From all, I think, it is possible to select 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 a 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 - need a page turning algorithm? Yes, you can sit and think for yourself. But why? This article has 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 of all, we automate the work with the creation of the index:

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


As seen in the Convert method, we serialize the POCO into a 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>(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<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; } } 


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

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


We now turn 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<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 - should be the foundation of the client. No, honestly, a smoother creation of an application framework on a client with the possibility of unit testing should still be looked for . I advise you to read my post on the development of Rx.

Authentication and Authorization


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

 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 the 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 iron. 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 building RESTful applications. To provide an API, we could, for example, write such 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 of the outputs was to write a separate controller for servicing AJAX requests.
Another is 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, detecting AJAX in code is not the cleanest code. We write API, right?
Exit MVC4 marked the new Web API functionality. At first glance, this is a mixture of MVC controllers and WCF Data Services.
I will not provide a tutorial on the Web API, there are many of them on the ASP.NET MVC site itself.
I will give only an example of the above code rewritten.
First, let's change the InsertOrUpdate method from the 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 we will write the controller itself:

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


So, a couple of moments, what has changed and how it works:

Just above, I indicated that the Web API is a mixture of MVC and WCF Data Services. But where is it expressed? It's simple - the new API supports OData! And works on a similar principle.
For example, to indicate 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 method GetAllProducts:

 public IQueryable<Product> 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 concentrate on creating the API itself.

Thanks for attention!

Source: https://habr.com/ru/post/143024/


All Articles