问题
Issue
We are currently having a problem of architecture on a WPF application. It concerns EntityFramework context management, it’s instantiated once and used during the entire life of the application. So we end up with a cache issue, entities are not updated when they were loaded once. Our entities are obsolete when using the application.
Technical specification
- Wpf project
- .Net Framework 4 client Profile
- MEF (Include in Framework 4.0 System.ComponentModel.Composition)
- Design pattern MVVM
- Multi users application
Architecture
This is a schema of the current architecture.
Service layer
- Manage calls to business rules (business layer)
- Save the context (through UnitOfWork) after business rules done
- Can be called only by a ViewModel
Business layer
- Define business rules
- Can be called only by service layer
Repository layer
- Execute methods which change context datas (insert, update , delete)
- Inherit ReadOnlyRepository
- Can be called only by business layer
ReadOnlyRepository layer
- Execute method which return datas (select)
- Can be called everywhere (ViewModel, Service layer, Business layer)
UnitOfWork
- Manage context instanciation
- Save context
- Context available only for repositories
Code
ViewModel
[Export(typeof(OrderViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class OrderViewModel : ViewModelBase
{
private readonly IOrderManagementService _orderManagementService;
private readonly IOrderReadOnlyRepository _orderReadOnlyRepository;
[ImportingConstructor]
public OrderViewModel(IOrderManagementService orderManagementService, IOrderReadOnlyRepository orderReadOnlyRepository)
{
_orderManagementService = orderManagementService;
_orderReadOnlyRepository = orderReadOnlyRepository;
}
}
Service layer
public class OrderManagementService : IOrderManagementService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IOrderManagementBusiness _orderManagementBusiness;
[ImportingConstructor]
public OrderManagementService (IUnitOfWork unitOfWork, IOrderManagementBusiness orderManagementBusiness)
{
_unitOfWork= unitOfWork;
_orderManagementBusiness = orderManagementBusiness;
}
}
Business layer
public class OrderManagementBusiness : IOrderManagementBusiness
{
private readonly IOrderReadOnlyRepository _orderReadOnlyRepository;
[ImportingConstructor]
public OrderManagementBusiness (IOrderReadOnlyRepository orderReadOnlyRepository)
{
_orderReadOnlyRepository = orderReadOnlyRepository;
}
}
ReadOnlyRepository layer
public class OrderReadOnlyRepository : ReadOnlyRepositoryBase<DataModelContainer, Order>, IOrderReadOnlyRepository
{
[ImportingConstructor]
public OrderReadOnlyRepository (IUnitOfWork uow) : base(uow)
{
}
}
ReadOnlyRepositoryBase
public abstract class ReadOnlyRepositoryBase<TContext, TEntity> : IReadOnlyRepository<TEntity>
where TEntity : class, IEntity
where TContext : DbContext
{
protected readonly TContext _context;
protected ReadOnlyRepositoryBase(IUnitOfWork uow)
{
_context = uow.Context;
}
protected DbSet<TEntity> DbSet
{
get { return _context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> GetAll(System.Linq.Expressions.Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "")
{
IQueryable<TEntity> query = DbSet.AsNoTracking();
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
return query.ToList();
}
public virtual IQueryable<TEntity> All()
{
return DbSet.AsNoTracking();
}
public virtual IQueryable<TEntity> AllWhere(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Where(predicate).AsNoTracking();
}
public virtual TEntity Get(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Where(predicate).AsNoTracking().FirstOrDefault();
}
public virtual TEntity GetById(int id)
{
TEntity find = DbSet.Find(id);
_context.Entry(find).State = System.Data.EntityState.Detached;
return DbSet.Find(id);
}
We can see that the context is given to the repository in the constructor. Select methods use the "AsNoTracking ()" method to not cache entities. It's a temporary solution which is obviously not viable in long term.
UnitOfWork
public class UnitOfWork : IUnitOfWork
{
private DataModelContainer _context;
public UnitOfWork()
: this(new DataModelContainer())
{
}
public UnitOfWork(DataModelContainer context)
{
_context = context;
}
public DataModelContainer Context
{
get { return _context; }
}
public int Save()
{
return _context.SaveChanges();
}
}
During the first composition of a service with MEF, UnitOfWork will be instantiated with the default constructor which instantiate the context.
Remarks
Some pieces of code have been omitted for readability.
Goal to achieve
The lifetime of the context is clearly an issue. Knowing that all calls within the same service method must share the same context.
How can we consider modifying the architecture to avoid having a single context ?
Feel free to ask questions ! If needed, I can attach a test project which highlight the issue.
回答1:
In your application there is only single unit of work but that is not he purpose of a unit a work. Instead, you need to create a unit of work each time "you work with the database". In your case the UnitOfWork
should not be part of the MEF container but you can create a UnitOfWorkFactory
and inject it from the container. Then the services can create a UnitOfWork
each time "work has to be done" with the database:
using (var unitOfWork = unitOfWorkFactory.Create()) {
// Do work ...
unitOfWork.Save();
}
I have modified UnitOfWork
so it implements IDisposable
. This will allow you to dispose the EF context and also perhaps rollback a transaction if Save
was not called. If you have no need for the extra transaction handling you can even get rid of the UnitOfWork
class because it simply wraps the EF context and instead you can used the EF context as a unit of work directly.
This change will change force you to modify how the service and the repositories are structured but you really have to because your issue is that you have a single unit of work that exists for the entire duration of the application.
回答2:
Outline clearly distinguished use cases, which would maintain own lifetime scope. This could help preventing other resources leaks as well (which are pretty frequent when using WPF).
Consider generic algorithm:
- Initialize lifetime scope.
- Using scope:
- Allocate views and other WPF resources, allocate business layer, data access (UoW, context, repo).
- Load data from db and display it to user.
- Wait for user action (1).
- Make some changes or load even more data from DB.
- Update data representation for user.
- Go to (1) until scenario is complete.
- Dispose scope, de-allocate resources.
The problem is that your scope currently is your application.
Now imagine that you manage scope at view level. You allocate, display view, get user's input, save changes and then the whole object tree is disposed at once.
Obviously, you should be flexible with scopes. Sometimes it can be useful to use it at view level (like "Edit item"), sometimes it could spread across several views (like wizard, for example). You can even maintain data-driven scopes (imagine you open a project in Visual Studio; begin lifetime scope to manage all resources, which should be available while project 'lives').
来源:https://stackoverflow.com/questions/26181559/wpf-entityframework-context-lifetime