WPF / EntityFramework Context Lifetime

天大地大妈咪最大 提交于 2019-12-23 10:17:41

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!