Home > OS >  How to abstract a EntityFramework Core DBSet<T> within an interface?
How to abstract a EntityFramework Core DBSet<T> within an interface?

Time:07-04

I am using EF Core 6, and my DbContext looks like this :

public class SpaceBattlesDatabase: DbContext, ISpaceBattlesDataBase
{
     public DbSet<Building> Buildings => Set<Building>();
     //   other irrelevant code
}

I would like to abstract it with an interface, to use in the central layer in a clean architecture..

public interface ISpaceBattlesDataBase
{
    public IQueryable<Building> Buildings { get; }
}

But the compilator warns me :

Error CS0738 'SpaceBattlesDatabase' does not implement interface member 'ISpaceBattlesDataBase.Buildings'. 'SpaceBattlesDatabase.Buildings' cannot implement 'ISpaceBattlesDataBase.Buildings' because it does not have the matching return type of 'IQueryable'.

But according to the documentation and the metadata, DbSet<TEntity> should implement IQueryable<TEntity>..

IEnumerable<T> doesn't work either.. Am I missing something here ?

How can I abstract the DbSet to use in the interface ?

Thank you

CodePudding user response:

Am I missing something here?

Yes, as explained by Eric Lippert here C# does not support virtual method return type covariance. In this particular case you can workaround with explicit interface implementation:

public class SpaceBattlesDatabase: DbContext, ISpaceBattlesDataBase
{
    public DbSet<Building> Buildings => Set<Building>();
    IQueryable<Building> ISpaceBattlesDataBase.Buildings => Buildings;
}

public interface ISpaceBattlesDataBase
{
    IQueryable<Building> Buildings { get; }
}

CodePudding user response:

I use a different approach in these cases. I first create a ContextFactory:

public class ContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
    public ApplicationDbContext CreateDbContext(string[]? args = null)
    {
        return new ApplicationDbContext();
    }
}

Then I create an IDataService interface:

public interface IDataService<T>
{
    Task<IEnumerable<T>> GetAll();
    Task<T> Get(int id);
    Task<T> Create(T entity);
    Task<bool> CreateMultiple(List<T> entity);
    Task<bool> Delete(T entity);
    Task<T> Update(T entity);
}

and implement it like this:

public class DataService<T> : IDataService<T> where T : class
{
    private readonly ContextFactory contextFactory;

    public DataService(ContextFactory contextFactory)
    {
        this.contextFactory = contextFactory;
    }

    public async Task<T> Create(T entity)
    {
        using ApplicationDbContext context = contextFactory.CreateDbContext();
        EntityEntry<T> createdResult = await context.Set<T>().AddAsync(entity);
        await context.SaveChangesAsync();
        return createdResult.Entity;
    }

    public async Task<bool> CreateMultiple(List<T> entity)
    {
        using ApplicationDbContext context = contextFactory.CreateDbContext();
        await context.Set<T>().AddRangeAsync(entity);
        await context.SaveChangesAsync();
        return true;
    }

    public async Task<bool> Delete(T entity)
    {
        try
        {
            using ApplicationDbContext context = contextFactory.CreateDbContext();
            context.Set<T>().Remove(entity);
            await context.SaveChangesAsync();
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

    public async Task<T> Get(int id)
    {
        using ApplicationDbContext context = contextFactory.CreateDbContext();
        return await context.Set<T>().FindAsync(id) ?? await new ValueTask<T>();
    }

    public async Task<IEnumerable<T>> GetAll()
    {
        using ApplicationDbContext context = contextFactory.CreateDbContext();
        return await context.Set<T>().ToListAsync();
    }

    public async Task<T> Update(T entity)
    {
        using ApplicationDbContext context = contextFactory.CreateDbContext();
        context.Set<T>().Update(entity);
        await context.SaveChangesAsync();
        return entity;
    }
}

You can modify it as you need. Finally, for the Building model, I would do something like:

public class BuildingCrudService
{
    private readonly IDataService<Building> dataService;

    public BuildingCrudService()
    {
        dataService = new DataService<Building>(new ContextFactory());
    }

    /// <summary>
    /// Adds a building to the database.
    /// </summary>
    /// <param name="building"></param>
    /// <returns>List of buildings.</returns>
    public async Task<List<Building>> AddBuildingAsync(Building building)
    {
        await dataService.Create(building);
        return await ListBuildingsAsync();
    }

    /// <summary>
    /// Deletes the building from the database.
    /// </summary>
    /// <param name="id"></param>
    /// <returns>True if deleted, else false.</returns>
    public async Task<bool> DeleteBuildingAsync(int id)
    {
        Building b = await GetBuildingByIDAsync(id);
        return await dataService.Delete(b);
    }

    /// <summary>
    /// Fetches the list of all the buildings from the database.
    /// </summary>
    /// <returns>List of buildings.</returns>
    public async Task<List<Building>> ListBuildingsAsync()
    {
        return (List<Building>)await dataService.GetAll();
    }

    /// <summary>
    /// Searches for a building that matches the id.
    /// </summary>
    /// <param name="id"></param>
    /// <returns>The building that matches the id.</returns>
    public async Task<Building> GetBuildingByIDAsync(int id)
    {
        return await dataService.Get(id);
    }

    /// <summary>
    /// Updates an existing building to the database.
    /// </summary>
    /// <param name="building"></param>
    /// <returns>The updated building.</returns>
    public async Task<Building> UpdateBuildingAsync(Building building)
    {
        Building b = await GetBuildingByIDAsync(building.BuildingId);
        b.Name = building.Name;
        // Also update other properties
        return await dataService.Update(b);
    }
}
  • Related