Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF / EntityFramework Context Lifetime

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.

architecture schema

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.

like image 957
toast Avatar asked Oct 03 '14 15:10

toast


People also ask

Should DbContext be singleton?

1 Answer. First, DbContext is a lightweight object; it is designed to be used once per business transaction. Making your DbContext a Singleton and reusing it throughout the application can cause other problems, like concurrency and memory leak issues. And the DbContext class is not thread safe.

Does DbContext need to be disposed?

Don't dispose DbContext objects. Although the DbContext implements IDisposable , you shouldn't manually dispose it, nor should you wrap it in a using statement. DbContext manages its own lifetime; when your data access request is completed, DbContext will automatically close the database connection for you.

Is AddDbContext scoped?

The AddDbContext extension method registers DbContext types with a scoped lifetime by default.

Is Entity Framework thread safe?

Entity Framework DB contexts are not thread safe. If an ASP.NET Core app wishes to process requests in a multi-threaded way while using Entity Framework Core, it's important to carefully manage DbContexts so that a single context isn't used on multiple threads simultaneously.


2 Answers

In your application there is only single unit of work but that is not the 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 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.

like image 154
Martin Liversage Avatar answered Oct 10 '22 06:10

Martin Liversage


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').

like image 32
mikalai Avatar answered Oct 10 '22 06:10

mikalai