Search code examples
c#entity-frameworkasync-awaitrepositoryunit-of-work

Async not working with EF + Unit of Work + Repo


I have tried to implement Unit of Work & Repo patterns alongside Entity Framework and ASP.net API project.

Firstly, I wanted to point out that I know that DbContext & DbSets are implementations of UoW & Repo patterns, but I am playing around to see what would fit my project.

PROBLEM

I have noticed that if I call async repo methods from my service, nothing happens, the method is called, but it seems to await is never triggered. If I call the method synchronously, everything is fine. (example of method is Count / CountAsync).

What is even more strange (and I cannot figure why) is that this same method call for some reason works in one service method, but not in another.

I will explain in more detail after I present my code.

PROJECT STRUCTURE

My project is structured in this way:

  • I have an API in which the Service layer is injected
  • In-Service are injected UoW & Repos
  • In Repo is injected UoW
  • Finally, UoW calls Database Context Factory which duty is to create new instance of my DbContex

--- CODE ---

Here is the current implementation, of course, some parts of the code were omitted for brevity.

Database Context Factory

/// <summary>
///     Interface for factory which is in charge of creating new DbContexts
/// </summary>
/// <autogeneratedoc />
public interface IDatabaseContextFactory
{
    /// <summary>
    /// Creates new Master Database Context.
    /// </summary>
    /// <returns>newly created MasterDatabaseContext</returns>
    /// <autogeneratedoc />
    DbContext MasterDbContext();
}


/// <inheritdoc />
/// <summary>
/// This is factory which is in charge of creating new DbContexts
/// It is implemented as Singleton as factory should be implemented (according to Gang of four) 
/// </summary>
/// <seealso cref="T:Master.Domain.DataAccessLayer.IDatabaseContextFactory" />
/// <autogeneratedoc />
public class DatabaseContextFactory : IDatabaseContextFactory
{
    /// <summary>
    /// This is implementation of singleton
    /// </summary>
    /// <remarks>
    /// To read more, visit: http://csharpindepth.com/Articles/General/Singleton.aspx (Jon skeet)
    /// </remarks>
    /// <autogeneratedoc />
    private static readonly DatabaseContextFactory instance = new DatabaseContextFactory();

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit (more about this: http://csharpindepth.com/Articles/General/Beforefieldinit.aspx)
    static DatabaseContextFactory()
    {
        
    }

    //so that class cannot be initiated 
    private DatabaseContextFactory()
    {
    }


    /// <summary>
    /// Instance of DatabaseContextFactory
    /// </summary>
    public static DatabaseContextFactory Instance => instance;

    /// <inheritdoc />
    /// <summary>
    /// Creates new MasterDatabaseContext
    /// </summary>
    /// <returns></returns>
    public DbContext MasterDbContext()
    {
        return new MasterDatabaseContext();
    }
}

Unit of Work

 /// <inheritdoc />
/// <summary>
/// Unit of work interface
/// Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
/// </summary>
/// <seealso cref="T:System.IDisposable" />
/// <autogeneratedoc />
public interface IUnitOfWork : IDisposable
{
    /// <summary>
    /// Gets the database context. DatabaseContext is part of EF and itself is implementation of UoW (and repo) patterns
    /// </summary>
    /// <value>
    /// The database context.
    /// </value>
    /// <remarks> 
    /// If true  UoW was implemented this wouldn't be here, but we are exposing this for simplicity sake. 
    /// For example so that repository  could use benefits of DbContext and DbSet <see cref="DbSet"/>. One of those benefits are Find and FindAsnyc methods
    /// </remarks>
    /// <autogeneratedoc />
    DbContext DatabaseContext { get; }
    /// <summary>
    /// Commits the changes to database
    /// </summary>
    /// <returns></returns>
    /// <autogeneratedoc />
    void Commit();
    
    /// <summary>
    /// Asynchronously commits changes to database.
    /// </summary>
    /// <returns></returns>
    /// <autogeneratedoc />
    Task CommitAsync();
 
}

 /// <inheritdoc />
/// <summary>
/// This is implementation of UoW pattern
/// </summary>
/// <remarks>
/// Martin Fowler: "Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems."
/// According to P of EEA, Unit of work should have following methods: commit(), registerNew((object), registerDirty(object), registerClean(object), registerDeleted(object)
/// The thing is DbContext is already implementation of UoW so there is no need to implement all this
/// In case that we were not using ORM all these methods would have been implemented
/// </remarks>
/// <seealso cref="T:Master.Domain.DataAccessLayer.UnitOfWork.IUnitOfWork" />
/// <autogeneratedoc />
public class UnitOfWork : IUnitOfWork
{
    /// <summary>
    /// Is instance already disposed
    /// </summary>
    /// <remarks>
    /// Default value of bool is false
    /// </remarks>
    /// <autogeneratedoc />
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="UnitOfWork"/> class.
    /// </summary>
    /// <param name="dbContextfactory">The database context factory.</param>
    /// <exception cref="ArgumentNullException">
    /// dbContextfactory
    /// or
    /// MasterDbContext - Master database context cannot be null
    /// </exception>
    /// <autogeneratedoc />
    public UnitOfWork(IDatabaseContextFactory dbContextfactory)
    {
        if (dbContextfactory == null)
        {
            throw new ArgumentNullException(nameof(dbContextfactory));
        }

        var MasterDbContext = dbContextfactory.MasterDbContext();

        if (MasterDbContext == null)
        {
            throw new ArgumentNullException(nameof(MasterDbContext), @"Master database context cannot be null");
        }

        DatabaseContext = MasterDbContext;
    }

    /// <summary>
    /// Gets the database context. DatabaseContext is part of EF and itself is implementation of UoW (and repo) patterns
    /// </summary>
    /// <value>
    /// The database context.
    /// </value>
    /// <remarks>
    /// If true  UoW was implemented this wouldn't be here, but we are exposing this for simplicity sake.
    /// For example so that repository  could use benefits of DbContext and DbSet <see cref="DbSet" />. One of those benefits are Find and FindAsnyc methods
    /// </remarks>
    /// <autogeneratedoc />
    public DbContext DatabaseContext { get; }

    /// <inheritdoc />
    /// <summary>
    /// Commits the changes to database
    /// </summary>
    /// <autogeneratedoc />
    public void Commit()
    {
         DatabaseContext.SaveChanges();
    }

    /// <inheritdoc />
    /// <summary>
    /// Asynchronously commits changes to database.
    /// </summary>
    /// <returns></returns>
    /// <autogeneratedoc />
    public async Task CommitAsync()
    {
        await DatabaseContext.SaveChangesAsync();
    }


    /// <inheritdoc />
    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <autogeneratedoc />
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Releases unmanaged and - optionally - managed resources.
    /// </summary>
    /// <param name="disposning"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
    /// <autogeneratedoc />
    protected virtual void Dispose(bool disposning)
    {
        if (_disposed)
            return;


        if (disposning)
        {
            DatabaseContext.Dispose();
        }


        _disposed = true;
    }

    /// <summary>
    /// Finalizes an instance of the <see cref="UnitOfWork"/> class.
    /// </summary>
    /// <autogeneratedoc />
    ~UnitOfWork()
    {
        Dispose(false);
    }
}

Generic Repository

/// <summary>
/// Generic repository pattern implementation
/// Repository  Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
/// </summary>
/// <remarks>
/// More info: https://martinfowler.com/eaaCatalog/repository.html
/// </remarks>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <autogeneratedoc />
public interface IMasterRepository<TEntity, in TKey> where TEntity : class
{
    /// <summary>
    ///     Gets entity (of type) from repository based on given ID
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns>Entity</returns>
    /// <autogeneratedoc />
    TEntity Get(TKey id);

    /// <summary>
    /// Asynchronously gets entity (of type) from repository based on given ID
    /// </summary>
    /// <param name="id">The identifier.</param>
    /// <returns></returns>
    /// <autogeneratedoc />
    Task<TEntity> GetAsnyc(TKey id);
    
    /// <summary>
    ///     Gets all entities of type from repository
    /// </summary>
    /// <returns></returns>
    /// <autogeneratedoc />
    IEnumerable<TEntity> GetAll();

    /// <summary>
    ///  Asynchronously gets all entities of type from repository
    /// </summary>
    /// <returns></returns>
    /// <autogeneratedoc />
    Task<IEnumerable<TEntity>> GetAllAsync();

    /// <summary>
    ///     Finds all entities of type which match given predicate
    /// </summary>
    /// <param name="predicate">The predicate.</param>
    /// <returns>Entities which satisfy conditions</returns>
    /// <autogeneratedoc />
    IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
}


//Note to self: according to P of EAA Repo plays nicely with QueryObject, Data mapper and Metadata mapper - Learn about those !!!



/// <summary>
/// Generic repository pattern implementation
/// Repository  Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <seealso cref="Master.Domain.DataAccessLayer.Repository.Generic.IMasterRepository{TEntity, TKey}" />
/// <inheritdoc cref="IMasterRepository{TEntity,TKey}" />
public class MasterRepository<TEntity, TKey> : IMasterRepository<TEntity, TKey>
    where TEntity : class 
{

    /// <summary>
    /// DbSet is part of EF, it holds entities of the context in memory, per EF guidelines DbSet was used instead of IDbSet 
    /// </summary>
    /// <remarks>
    /// <para>
    /// Even though we are not 100% happy about this, 
    /// We decided to go with this instead of (for example) IEnumerable so that we can use benefits of <see cref="DbSet"/>
    /// Those benefits for example are Find and FindAsync methods which are much faster in fetching entities by their key than for example Single of First methods
    /// </para>
    /// </remarks>
    /// <autogeneratedoc />
    private readonly DbSet<TEntity> _dbSet;


    /// <summary>
    /// Initializes a new instance of the <see cref="MasterRepository{TEntity, TKey}"/> class.
    /// </summary>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <exception cref="ArgumentNullException">unitOfWork - Unit of work cannot be null</exception>
    /// <autogeneratedoc />
    public MasterRepository(IUnitOfWork unitOfWork)
    {
        if (unitOfWork == null)
        {
            throw new ArgumentNullException(nameof(unitOfWork), @"Unit of work cannot be null");
        }

        _dbSet = unitOfWork.DatabaseContext.Set<TEntity>();
    }

    /// <inheritdoc />
    /// <summary>
    /// Gets entity with given key
    /// </summary>
    /// <param name="id">The key of the entity</param>
    /// <returns>Entity with key id</returns>
    public TEntity Get(TKey id)
    {
        return _dbSet.Find(id);
    }

    /// <inheritdoc />
    /// <summary>
    /// Asynchronously gets entity with given key
    /// </summary>
    /// <param name="id">The key of the entity</param>
    /// <returns>Entity with key id</returns>
    public async Task<TEntity> GetAsnyc(TKey id)
    {
         return await _dbSet.FindAsync(id);
    }

    /// <inheritdoc />
    /// <summary>
    /// Gets all entities
    /// </summary>
    /// <returns>List of entities of type TEntiy</returns>
    public IEnumerable<TEntity> GetAll()
    {
        return _dbSet.ToList();
    }

    public async Task<IEnumerable<TEntity>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();

    }

    public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
    {
        return _dbSet.Where(predicate).ToList();
    }

}

Saved Movies Repository

 /// <inheritdoc />
/// <summary>
/// Repository for dealing with <see cref="T:Master.Domain.Model.MovieAggregate.SavedMovie" /> entity
/// </summary>
/// <seealso cref="!:Master.Domain.DataAccessLayer.Repository.Generic.IMasterRepository{Master.Domain.Model.MovieAggregate.SavedMovie,System.Guid}" />
/// <autogeneratedoc />
public interface ISavedMoviesRepository : IMasterRepository<SavedMovie, Guid>
{
    /// <summary>
    /// Asynchronously Gets number of saved Movies for the user
    /// </summary>
    /// <param name="user">The user.</param>
    /// <returns>Number of saved Movies</returns>
    /// <autogeneratedoc />
    Task<int> CountForUser(Model.UserAggregate.User user);
}


/// <inheritdoc cref="ISavedMoviesRepository" />
/// />
/// <summary>
///     Repository for dealing with <see cref="T:Master.Domain.Model.MovieAggregate.SavedMovie" /> entity
/// </summary>
/// <seealso cref="!:Master.Domain.DataAccessLayer.Repository.Generic.MasterRepository{Master.Domain.Model.MovieAggregate.SavedMovie, System.Guid}" />
/// <seealso cref="T:Master.Domain.DataAccessLayer.Repository.SavedMovies.ISavedMoviesRepository" />
/// <autogeneratedoc />
public class SavedMovieRepository : MasterRepository<SavedMovie, Guid>, ISavedMoviesRepository
{
    /// <summary>
    ///     Ef's DbSet - in-memory collection for dealing with entities
    /// </summary>
    /// <autogeneratedoc />
    private readonly DbSet<SavedMovie> _dbSet;
    private readonly IUnitOfWork _unitOfWork;



    /// <inheritdoc />
    /// <summary>
    ///     Initializes a new instance of the
    ///     <see cref="T:Master.Domain.DataAccessLayer.Repository.SavedMovies.SavedMovieRepository" /> class.
    /// </summary>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <exception cref="T:System.ArgumentNullException"></exception>
    /// <autogeneratedoc />
    public SavedMovieRepository(UnitOfWork.UnitOfWork unitOfWork) : base(unitOfWork)
    {
        if (unitOfWork == null)
            throw new ArgumentNullException();
        _dbSet = unitOfWork.DatabaseContext.Set<SavedMovie>();
        _unitOfWork = unitOfWork;

    }

    /// <inheritdoc />
    /// <summary>
    ///     Asynchronously Gets number of saved Movies for the user
    /// </summary>
    /// <param name="user">The user.</param>
    /// <returns>
    ///     Number of saved Movies
    /// </returns>
    /// <exception cref="T:System.ArgumentNullException">user - User cannot be null</exception>
    /// <autogeneratedoc />
    public async Task<int> CountForUser(Model.UserAggregate.User user)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user), @"User cannot be null");

        return await _dbSet.CountAsync(r => r.UserWhoSavedId == user.Id);
    }
}

Saved movies service

/// <inheritdoc />
/// <summary>
///     This is service for handling saved Movies!
/// </summary>
/// <seealso cref="T:Master.Infrastructure.Services.SavedMovieService.Interfaces.ISavedMovieService" />
/// <autogeneratedoc />
public class SavedMovieService : ISavedMovieService
{
    /// <summary>
    /// The saved Movies repository <see cref="ISavedMoviesRepository"/>
    /// </summary>
    /// <autogeneratedoc />
    private readonly ISavedMoviesRepository _savedMoviesRepository;
   
    /// <summary>
    /// The unit of work <see cref="IUnitOfWork"/>
    /// </summary>
    /// <autogeneratedoc />
    private readonly IUnitOfWork _unitOfWork;

    /// <summary>
    /// The user repository <see cref="IUserRepository"/>
    /// </summary>
    /// <autogeneratedoc />
    private readonly IUserRepository _userRepository;

    /// <summary>
    /// Initializes a new instance of the <see cref="SavedMovieService"/> class.
    /// </summary>
    /// <param name="savedMoviesRepository">The saved Movies repository.</param>
    /// <param name="userRepository">The user repository.</param>
    /// <param name="unitOfWork">The unit of work.</param>
    /// <autogeneratedoc />
    public SavedMovieService(ISavedMoviesRepository savedMoviesRepository, IUserRepository userRepository,
        IUnitOfWork unitOfWork)
    {
        _savedMoviesRepository = savedMoviesRepository;
        _userRepository = userRepository;
        _unitOfWork = unitOfWork;
    }

    public Task<int> CountNumberOfSavedMoviesForUser(string userId)
    {
        if (string.IsNullOrEmpty(userId))
            throw new ArgumentNullException(nameof(userId), @"User id must not be empty");


        var user = _userRepository.Get(userId);
        return _savedMoviesRepository.CountForUser(user);
    }
    
      public async Task<Guid> SaveWorkoutFromLibraryAsync(string userWhoIsSavingId, Guid galleryId,
        bool isUserPro)
    {
        if (string.IsNullOrEmpty(userWhoIsSavingId))
            throw new ArgumentNullException(nameof(userWhoIsSavingId), @"User id cannot be empty");

        if (galleryId == Guid.Empty)
            throw new ArgumentException(@"Id of gallery cannot be empty", nameof(galleryId));

            //get user who is saving from DB
            var userWhoIsSaving = _userRepository.Get(userWhoIsSavingId);
           

            if (userWhoIsSaving == null)
                throw new ObjectNotFoundException($"User with provided id not found - id: {userWhoIsSavingId}");

            //how many movies has this user saved so far
            var numberOfAlreadySavedMoviesForUser = await _savedWorkoutsRepository.CountForUserAsync(userWhoIsSaving);

            // more code here
    }

}

Web Api Controller

[Authorize]
[RoutePrefix("api/Saved")]
[ApiVersion("2.0")]
public class SavedController : ApiController
{
    private readonly ISavedMovieService _savedMovieService;



    /// <inheritdoc />
    /// <summary>
    ///     Initializes a new instance of the <see cref="T:Master.Infrastructure.Api.V2.Controllers.SavedController" /> class.
    /// </summary>
    /// <param name="savedMovieService">The saved Movie service.</param>
    /// <autogeneratedoc />
    public SavedController(ISavedMovieService savedMovieService)
    {        
        _savedMovieService = savedMovieService;
    }

    public async Task<IHttpActionResult> GetNumberOfSavedForUser()
    {
        var cnt = await _savedMovieService.CountNumberOfSavedMoviesForUser(User.Identity.GetUserId());

        return Ok(cnt);
    }
    
    public async Task<IHttpActionResult> SaveFromGalery(SaveModel model)
    {
        await _savedMovieService.SaveWorkoutFromGalleryAsync(User.Identity.GetUserId(), model.Id, model.IsPro);

        return Ok();
    }
}

Ninject Configuration

(Only important parts)

        kernel.Bind<MasterDatabaseContext>().ToSelf().InRequestScope();
        kernel.Bind<IDatabaseContextFactory>().ToMethod(c => DatabaseContextFactory.Instance).InSingletonScope();
        kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope();

        kernel.Bind(typeof(IMasterRepository<,>)).To(typeof(MasterRepository<,>));

        kernel.Bind<ISavedMoviesRepository>().To<SavedMovieRepository>();
        kernel.Bind<IUserRepository>().To<UserRepository>();
        kernel.Bind<ISavedMovieService>().To<SavedMovieService>();

I wanted to point out that I have a couple of more repositories(4 in total including Saved and User) injected in SavedService, but I don't believe they are relevant as they are pretty much the same as SavedRepo, but if need be, I can also add them. Also, this is the only service that currently implements this pattern and methods.

So here is what is happening when I call SaveFromGalery:

  1. UoW constructor is called
  2. DatabaseContextFactory MasterDbContext() is called
  3. MasterRepository constructor is called
  4. SavedMoviesRepository constructor is called
  5. UoW constructor is called (again 2nd time)
  6. DatabaseContextFactory MasterDbContext() is called (2nd time)
  7. MasterRepository constructor is called (again 2nd time)
  8. UserRepository is called
  9. MasterRepository constructor is called (again 3rd time)
  10. MasterRepository constructor is called (again 4th time)
  11. SavedService constructor is called
  12. HTTP GET SaveFromGalery is called
  13. user is fetched from user repo successfully
  14. _savedWorkoutsRepository.CountForUserAsync is called
  15. program enters the method hits the await but never returns the result

On the other hand, when GetNumberOfSavedForUser is called, here is what happens:

  1. 1 - 11 steps are the same
  2. HTTP GET GetNumberOfSavedForUser is called
  3. user is fetched from user repo successfully
  4. _savedWorkoutsRepository.CountForUserAsync is called SUCCESSFULLY
  5. UoW Dispose is called
  6. UoW dispose of is called

Also as stated earlier if, _savedWorkoutsRepository.CountForUserAsync is made synchronous everything proceeds fine.

Could someone please help me out to figure what exactly is happening ?


Solution

  • You're probably using Wait or Result in your real-world code (not the code posted here, which is incomplete). This will cause a deadlock on ASP.NET classic.

    Specifically, what happens is that when you pass a task to await, by default it will capture the "current context" and use that to resume the asynchronous method when that task completes. Then the code blocks on a task (i.e., Wait or Result). The problem is that the context on ASP.NET classic only allows in one thread at a time. So as long as that thread is blocked on the task, it's "taking up" that context, which is actually preventing the task from completing. Hence, deadlock.

    Note that ConfigureAwait(false) is not a fix; it is at best a workaround. The proper fix is to replace Wait / Result with await.