[转]Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0
本文转自:http://www.tuicool.com/articles/BBVr6z
Thanks to everyone for allowing us to give back to the .NET community, we released v1.0 of the Generic Unit of Work and Repository Framework for four weeks and received 655 downloads and 4121 views. This post will also serve as the documentation for release v2.0. Thanks to Ivan ( @ifarkas ) for helping out on the Async development and Ken for life the Unit of Work life cycle management and scaling the framework to handle Bounded DbContexts.
This will be part five of a five part series of blog posts.
- Generically Implementing the Unit of Work & Repository Pattern with Entity Framework in MVC & Simplifying Entity Graphs
- MVC 4, Kendo UI, SPA with Layout, View, Router & MVVM
- MVC 4, Web API, OData, EF, Kendo UI, Grid, Datasource (CRUD) with MVVM
- MVC 4, Web API, OData, EF, Kendo UI, Binding a Form to Datasource (CRUD) with MVVM
- Upgrading to Async with Entity Framework, MVC, OData AsyncEntitySetController, Kendo UI, Glimpse & Generic Unit of Work Repository Framework v2.0
We’ll continue on from the most recent post in this series, you can do a quick review of it herehttp://blog.longle.net/2013/06/19/mvc-4-web-api-odata-ef-kendo-ui-binding-a-form-to-datasource-crud-with-mvvm-part . Now let’s get right into it, by first taking a look at what was all involved on the server side.
First off let’s take a quick look and the changes we made to our DbContextBase to support Async.
Repository.DbContextBase.cs
Before
public class DbContextBase : DbContext, IDbContext
{
private readonly Guid _instanceId;
public DbContextBase(string nameOrConnectionString) : base(nameOrConnectionString)
{
_instanceId = Guid.NewGuid();
}
public Guid InstanceId
{
get { return _instanceId; }
}
public void ApplyStateChanges()
{
foreach (var dbEntityEntry in ChangeTracker.Entries())
{
var entityState = dbEntityEntry.Entity as IObjectState;
if (entityState == null)
throw new InvalidCastException("All entites must implement the IObjectState interface, " +
"this interface must be implemented so each entites state can explicitely determined when updating graphs.");
dbEntityEntry.State = StateHelper.ConvertState(entityState.State);
}
}
public new IDbSet<T> Set<T>() where T : class
{
return base.Set<T>();
}
protected override void OnModelCreating(DbModelBuilder builder)
{
builder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(builder);
}
public override int SaveChanges()
{
ApplyStateChanges();
return base.SaveChanges();
}
}
After:
public class DbContextBase : DbContext, IDbContext
{
private readonly Guid _instanceId;
public DbContextBase(string nameOrConnectionString) : base(nameOrConnectionString)
{
_instanceId = Guid.NewGuid();
}
public Guid InstanceId
{
get { return _instanceId; }
}
public void ApplyStateChanges()
{
foreach (DbEntityEntry dbEntityEntry in ChangeTracker.Entries())
{
var entityState = dbEntityEntry.Entity as IObjectState;
if (entityState == null)
throw new InvalidCastException("All entites must implement the IObjectState interface, " +
"this interface must be implemented so each entites state can explicitely determined when updating graphs.");
dbEntityEntry.State = StateHelper.ConvertState(entityState.State);
}
}
public new IDbSet<T> Set<T>() where T : class
{
return base.Set<T>();
}
public override int SaveChanges()
{
ApplyStateChanges();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync()
{
ApplyStateChanges();
return base.SaveChangesAsync();
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken)
{
ApplyStateChanges();
return base.SaveChangesAsync(cancellationToken);
}
protected override void OnModelCreating(DbModelBuilder builder)
{
builder.Conventions.Remove<PluralizingTableNameConvention>();
base.OnModelCreating(builder);
}
}
All that was needed here was to expose all the DbContext Async save operations so that we could use with our IUnitOfWork implementation, and also not forgetting to invoke our ApplyStateChanges so that we are managing the different states each entity could have when dealing with graphs.
Next up, are the enhancements made to our Repository.cs, so that our generic repositories can leverage the Async goodness as well.
Repostiory.Repository.cs
Before:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private readonly Guid _instanceId;
internal IDbContext Context;
internal IDbSet<TEntity> DbSet;
public Repository(IDbContext context)
{
Context = context;
DbSet = context.Set<TEntity>();
_instanceId = Guid.NewGuid();
}
public Guid InstanceId
{
get { return _instanceId; }
}
public virtual TEntity FindById(object id)
{
return DbSet.Find(id);
}
public virtual void InsertGraph(TEntity entity)
{
DbSet.Add(entity);
}
public virtual void Update(TEntity entity)
{
DbSet.Attach(entity);
}
public virtual void Delete(object id)
{
var entity = DbSet.Find(id);
((IObjectState) entity).State = ObjectState.Deleted;
Delete(entity);
}
public virtual void Delete(TEntity entity)
{
DbSet.Attach(entity);
DbSet.Remove(entity);
}
public virtual void Insert(TEntity entity)
{
DbSet.Attach(entity);
}
public virtual IRepositoryQuery<TEntity> Query()
{
var repositoryGetFluentHelper = new RepositoryQuery<TEntity>(this);
return repositoryGetFluentHelper;
}
internal IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
List<Expression<Func<TEntity, object>>> includeProperties = null,
int? page = null,
int? pageSize = null)
{
IQueryable<TEntity> query = DbSet;
if (includeProperties != null)
includeProperties.ForEach(i => query = query.Include(i));
if (filter != null)
query = query.Where(filter);
if (orderBy != null)
query = orderBy(query);
if (page != null && pageSize != null)
query = query
.Skip((page.Value - 1)*pageSize.Value)
.Take(pageSize.Value);
var results = query;
return results;
}
}
After:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
private readonly Guid _instanceId;
private readonly DbSet<TEntity> _dbSet;
public Repository(IDbContext context)
{
_dbSet = context.Set<TEntity>();
_instanceId = Guid.NewGuid();
}
public Guid InstanceId
{
get { return _instanceId; }
}
public virtual TEntity Find(params object[] keyValues)
{
return _dbSet.Find(keyValues);
}
public virtual async Task<TEntity> FindAsync(params object[] keyValues)
{
return await _dbSet.FindAsync(keyValues);
}
public virtual async Task<TEntity> FindAsync(CancellationToken cancellationToken, params object[] keyValues)
{
return await _dbSet.FindAsync(cancellationToken, keyValues);
}
public virtual IQueryable<TEntity> SqlQuery(string query, params object[] parameters)
{
return _dbSet.SqlQuery(query, parameters).AsQueryable();
}
public virtual void InsertGraph(TEntity entity)
{
_dbSet.Add(entity);
}
public virtual void Update(TEntity entity)
{
_dbSet.Attach(entity);
((IObjectState)entity).State = ObjectState.Modified;
}
public virtual void Delete(object id)
{
var entity = _dbSet.Find(id);
Delete(entity);
}
public virtual void Delete(TEntity entity)
{
_dbSet.Attach(entity);
((IObjectState)entity).State = ObjectState.Deleted;
_dbSet.Remove(entity);
}
public virtual void Insert(TEntity entity)
{
_dbSet.Attach(entity);
((IObjectState)entity).State = ObjectState.Added;
}
public virtual IRepositoryQuery<TEntity> Query()
{
var repositoryGetFluentHelper = new RepositoryQuery<TEntity>(this);
return repositoryGetFluentHelper;
}
internal IQueryable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
List<Expression<Func<TEntity, object>>> includeProperties = null,
int? page = null,
int? pageSize = null)
{
IQueryable<TEntity> query = _dbSet;
if (includeProperties != null)
{
includeProperties.ForEach(i => query = query.Include(i));
}
if (filter != null)
{
query = query.Where(filter);
}
if (orderBy != null)
{
query = orderBy(query);
}
if (page != null && pageSize != null)
{
query = query
.Skip((page.Value - 1)*pageSize.Value)
.Take(pageSize.Value);
}
return query;
}
internal async Task<IEnumerable<TEntity>> GetAsync(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
List<Expression<Func<TEntity, object>>> includeProperties = null,
int? page = null,
int? pageSize = null)
{
return Get(filter, orderBy, includeProperties, page, pageSize).AsEnumerable();
}
}
Here we’ve exposed the FindAsync methods from DbSet, so our Repositories can make use of them, and we’ve also wrapped implemented an Async implementation of our Get() method so that we can use it in our new Web Api ProductController.cs later.
Important note: here is that although our method is named GetAsync, it is not truly performing an Async interaction, this is due to the fact that if we were to use ToListAsync(), we would already executed the the query prior to OData applying it’s criteria to the execution plan e.g. if the OData query was requesting 10 records for page 2 of a grid from a Products table that had 1000 rows in it, ToListAsync() would have actually pulled a 1000 records from SQL to the web server and at that time do a skip 10 and take 20 from the collection of Products with 1000 objects. What we want is for this to happen on the SQL Server, meaning, SQL query the Products table, skip the first 10, and take next 10 records and only send those 10 records over to the web server, which will eventually surface into the Grid in the user’s browsers. Hence we are favoring payload size (true SQL Server side paging) going over the wire, vs. a true Async call to SQL.
Northwind.Web.Areas.Spa.Api.ProductController.cs
Before:
public class ProductController : EntitySetController<Product, int>
{
private readonly IUnitOfWork _unitOfWork;
public ProductController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public override IQueryable<Product> Get()
{
return _unitOfWork.Repository<Product>().Query().Get();
}
protected override Product GetEntityByKey(int key)
{
return _unitOfWork.Repository<Product>().FindById(key);
}
protected override Product UpdateEntity(int key, Product update)
{
update.State = ObjectState.Modified;
_unitOfWork.Repository<Product>().Update(update);
_unitOfWork.Save();
return update;
}
public override void Delete([FromODataUri] int key)
{
_unitOfWork.Repository<Product>().Delete(key);
_unitOfWork.Save();
}
protected override void Dispose(bool disposing)
{
_unitOfWork.Dispose();
base.Dispose(disposing);
}
}
After:
[ODataNullValue]
public class ProductController : AsyncEntitySetController<Product, int>
{
private readonly IUnitOfWork _unitOfWork;
public ProductController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
protected override void Dispose(bool disposing)
{
_unitOfWork.Dispose();
base.Dispose(disposing);
}
protected override int GetKey(Product entity)
{
return entity.ProductID;
}
[Queryable]
public override async Task<IEnumerable<Product>> Get()
{
return await _unitOfWork.Repository<Product>().Query().GetAsync();
}
[Queryable]
public override async Task<HttpResponseMessage> Get([FromODataUri] int key)
{
var query = _unitOfWork.Repository<Product>().Query().Filter(x => x.ProductID == key).Get();
return Request.CreateResponse(HttpStatusCode.OK, query);
}
///// <summary>
///// Retrieve an entity by key from the entity set.
///// </summary>
///// <param name="key">The entity key of the entity to retrieve.</param>
///// <returns>A Task that contains the retrieved entity when it completes, or null if an entity with the specified entity key cannot be found in the entity set.</returns>
[Queryable]
protected override async Task<Product> GetEntityByKeyAsync(int key)
{
return await _unitOfWork.Repository<Product>().FindAsync(key);
}
protected override async Task<Product> CreateEntityAsync(Product entity)
{
if (entity == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
_unitOfWork.Repository<Product>().Insert(entity);
await _unitOfWork.SaveAsync();
return entity;
}
protected override async Task<Product> UpdateEntityAsync(int key, Product update)
{
if (update == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
if (key != update.ProductID)
throw new HttpResponseException(Request.CreateODataErrorResponse(HttpStatusCode.BadRequest, new ODataError { Message = "The supplied key and the Product being updated do not match." }));
try
{
update.State = ObjectState.Modified;
_unitOfWork.Repository<Product>().Update(update);
var x = await _unitOfWork.SaveAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
return update;
}
// PATCH <controller>(key)
/// <summary>
/// Apply a partial update to an existing entity in the entity set.
/// </summary>
/// <param name="key">The entity key of the entity to update.</param>
/// <param name="patch">The patch representing the partial update.</param>
/// <returns>A Task that contains the updated entity when it completes.</returns>
protected override async Task<Product> PatchEntityAsync(int key, Delta<Product> patch)
{
if (patch == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
if (key != patch.GetEntity().ProductID)
throw Request.EntityNotFound();
var entity = await _unitOfWork.Repository<Product>().FindAsync(key);
if (entity == null)
throw Request.EntityNotFound();
try
{
patch.Patch(entity);
await _unitOfWork.SaveAsync();
}
catch (DbUpdateConcurrencyException)
{
throw new HttpResponseException(HttpStatusCode.Conflict);
}
return entity;
}
public override async Task Delete([FromODataUri] int key)
{
var entity = await _unitOfWork.Repository<Product>().FindAsync(key);
if (entity == null)
throw Request.EntityNotFound();
_unitOfWork.Repository<Product>().Delete(entity);
try
{
await _unitOfWork.SaveAsync();
}
catch (Exception e)
{
throw new HttpResponseException(
new HttpResponseMessage(HttpStatusCode.Conflict)
{
StatusCode = HttpStatusCode.Conflict,
Content = new StringContent(e.Message),
ReasonPhrase = e.InnerException.InnerException.Message
});
}
}
#region Links
// Create a relation from Product to Category or Supplier, by creating a $link entity.
// POST <controller>(key)/$links/Category
// POST <controller>(key)/$links/Supplier
/// <summary>
/// Handle POST and PUT requests that attempt to create a link between two entities.
/// </summary>
/// <param name="key">The key of the entity with the navigation property.</param>
/// <param name="navigationProperty">The name of the navigation property.</param>
/// <param name="link">The URI of the entity to link.</param>
/// <returns>A Task that completes when the link has been successfully created.</returns>
[AcceptVerbs("POST", "PUT")]
public override async Task CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
var entity = await _unitOfWork.Repository<Product>().FindAsync(key);
if (entity == null)
throw Request.EntityNotFound();
switch (navigationProperty)
{
case "Category":
var categoryKey = Request.GetKeyValue<int>(link);
var category = await _unitOfWork.Repository<Category>().FindAsync(categoryKey);
if (category == null)
throw Request.EntityNotFound();
entity.Category = category;
break;
case "Supplier":
var supplierKey = Request.GetKeyValue<