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.
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 - 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:
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:
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
following CRUD pages and abstract repository will be generated:
And also its implementation:
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:
In the NinjectMVC3.cs file in the CreateKernel method, write:
Now write our controller:
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:
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:
As you can see in the Convert method, we serialize POCO in the Lucene Document.
Controller Code:
To display the results, create a ResultDefinition:
This is where POCO deserializes.
And finally, we automate the work with requests:
Now let's move on to the controller:
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.
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:
And here is Profile_GetProfiles from InstallProfile.SQL:
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 MVC is a great framework for creating RESTful applications. To provide the API, we could, for example, write the following code:
Yes, one way out was to write a separate controller to serve AJAX requests.
The other is the spaghetti code:
And if you also need to add CRUD operations, then:
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:
And write the controller itself:
So, a couple of points, what has changed and how it works:
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:
Now you just need to change the GetAllProducts method:
And in the browser, for example, type the following:
Thus, we got rid of distractions and can now focus on creating the API itself.
Thanks for attention!
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
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!