The repository pattern for entity framework Core. Any questions?

I always used the repository pattern, but more as a wrapper that was needed for unit tests, you could embed caching there through a Decorator and some other decarable functionality, well, that's all, I didn't see any more benefits.

My doubts and questions about the classical implementation of the pattern:

  1. The context dependency is not only in the project, where the data access layer is used (a specific repository for a specific application). storage technologies), but also in the main project where the Ef context had to be registered in DI. So that later the repository gets the context in the constructor. Isn't that weird? The ideology implies using an abstract repository as a universal access to data, for example, 10 repositories implement IGenericDataRepository and each repository (project) has its own dependencies and its own data storage model. That is the EFCore context should only be used within the project that implements the system storage based on EFCore technology. the only connection to the outside world is the settings and the connection string. But basically everyone uses this version of the repository pattern (even an example is given on the EFCore off-site).

  2. Some people put out IQuerible and not Ienumerable-then why bother with something, the DbSet interface is so good.

I tried to make the most abstract repository that can be replaced in DI with different implementations storage technologies (SQL via EFCore, store in XML file, store in NoSQL). Below I will give the structure of the project and the code, please express your opinion, Because a project with a complex data layer has appeared and I am again thinking about the Repository. You probably won't have to replace EfCoreRepository with another storage system, but you still want to work with data as independently and abstractly as possible.

Project structure

DAL.Abstract project contains:

IGenericDataRepository.cs - abstract repository interface

    public interface IGenericDataRepository<T>
    {
        T GetById(int id);
        Task<T> GetByIdAsync(int id);

        T GetSingle(Expression<Func<T, bool>> predicate);
        Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate);
        IEnumerable<T> GetWithInclude(params Expression<Func<T, object>>[] includeProperties); //?????

        IEnumerable<T> List();
        IEnumerable<T> List(Expression<Func<T, bool>> predicate);
        Task<IEnumerable<T>> ListAsync();
        Task<IEnumerable<T>> ListAsync(Expression<Func<T, bool>> predicate);

        int Count(Expression<Func<T, bool>> predicate);
        Task<int> CountAsync(Expression<Func<T, bool>> predicate);

        void Add(T entity);
        Task AddAsync(T entity);

        void AddRange(IEnumerable<T> entitys); 
        Task AddRangeAsync(IEnumerable<T> entitys); 

        void Delete(T entity);
        void Delete(Expression<Func<T, bool>> predicate);
        Task DeleteAsync(T entity);
        Task DeleteAsync(Expression<Func<T, bool>> predicate);

        void Edit(T entity);
        Task EditAsync(T entity);

        bool IsExist(Expression<Func<T, bool>> predicate);
        Task<bool> IsExistAsync(Expression<Func<T, bool>> predicate);
    }

IRepository.cs - Interfaces for specific repositories. Suddenly you need a sooo specific method of working with data for a specific repository and not to add it to IGenericDataRepository.

public interface ISerialPortOptionRepository : IGenericDataRepository<SerialOption>
{  
}
public interface ITcpIpOptionRepository : IGenericDataRepository<TcpIpOption>
{
}

public interface IHttpOptionRepository : IGenericDataRepository<HttpOption>
{
}

public interface IExchangeOptionRepository : IGenericDataRepository<ExchangeOption>
{
}
public interface IDeviceOptionRepository : IGenericDataRepository<DeviceOption>
{
}

In the folders Entities the data model to be saved in the repository. The model is clean (PROPERTIES WITHOUT ATTRIBUTES AND ADDITIONAL PRIBLUD SPECIFIC TO THE STORAGE SYSTEM)

DeviceOption.cs - some kind of business logic-friendly data model

    public class DeviceOption : EntityBase
    {
        public string Name { get; set; }
        public string TopicName4MessageBroker { get; set; }         
        public string Description { get; set; }
        public bool AutoBuild { get; set; }                        
        public bool AutoStart{ get; set; }                
        public List<string> ExchangeKeys { get; set; }
    }

The project DAL.EFCore contains THE IMPLEMENTATION OF A SPECIFIC STORAGE TECHNOLOGY

In the Entities folder, A data model IN WHICH IT IS CONVENIENT TO STORE DATA FOR A SPECIFIC TECHNOLOGY (IN THIS CASE, FOR EFCore).

EfDeviceOption.cs - the same model (DeviceOption), only for the storage system, in a form that is understandable for it.

public class EfDeviceOption : IEntity
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    [Required]
    [MaxLength(256)]
    public string Name { get; set; }

    [Required]
    [MaxLength(256)]
    public string TopicName4MessageBroker { get; set; }         

    [Required]
    public string Description { get; set; }
    public bool AutoBuild { get; set; }                         
    public bool AutoStart { get; set; }                        


    private string _exchangeKeysMetaData;
    [NotMapped]
    public string[] ExchangeKeys
    {
        get => _exchangeKeysMetaData.Split(';');
        set => _exchangeKeysMetaData = string.Join($"{';'}", value);
    }
}

Context.cs - the data context for EFCore.

public sealed class Context : Microsoft.EntityFrameworkCore.DbContext
{
    private readonly string _connStr;  // строка подключенния

    #region Reps

    public DbSet<EfSerialOption> SerialPortOptions { get; set; }
    public DbSet<EfTcpIpOption> TcpIpOptions { get; set; }
    public DbSet<EfHttpOption> HttpOptions { get; set; }
    public DbSet<EfDeviceOption> DeviceOptions { get; set; }
    public DbSet<EfExchangeOption> ExchangeOptions { get; set; }

    #endregion

    #region ctor

    public Context(string connStr)
    {
        _connStr = connStr;
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        Database.EnsureCreated();
    }

    #endregion

    #region Config

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connStr);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
       modelBuilder.ApplyConfiguration(new EfDeviceOptionConfig());
       modelBuilder.ApplyConfiguration(new EfExchangeOptionConfig());
       modelBuilder.ApplyConfiguration(new EfHttpOptionConfig());
       base.OnModelCreating(modelBuilder);
    }

    #endregion
}

DesignTimeDbContextFactory.cs - context creation factory for the migration system AutoMapperConfig.cs - configuring mapping BETWEEN MODELS DAL.Abstract And DAL.EFCore

IN THE FOLDER Repository IMPLEMENTATION OF SPECIFIC REPOSITORIES

EfBaseRepository.cs - the base repository class for EFCore

/// <summary>
/// Базовый тип репозитория для EntitiFramework
/// </summary>
/// <typeparam name="TDb">Тип в системе хранения</typeparam>
/// <typeparam name="TMap">Тип в бизнесс логики</typeparam>
public abstract class EfBaseRepository<TDb, TMap> : IDisposable
                                                    where TDb : class, IEntity
                                                    where TMap : class
{
    #region field
    protected readonly Context Context;
    protected readonly DbSet<TDb> DbSet;
    #endregion


    #region ctor
    protected EfBaseRepository(string connectionString)
    {
        Context = new Context(connectionString);
        DbSet = Context.Set<TDb>();
    }
    static EfBaseRepository()
    {
        AutoMapperConfig.Register();
    }
    #endregion


    #region CRUD
    protected TMap GetById(int id)
    {
        var efSpOption = DbSet.Find(id);
        var spOptions = AutoMapperConfig.Mapper.Map<TMap>(efSpOption);
        return spOptions;
    }

    protected async Task<TMap> GetByIdAsync(int id)
    {
        var efSpOption = await DbSet.FindAsync(id);
        var spOptions = AutoMapperConfig.Mapper.Map<TMap>(efSpOption);
        return spOptions;
    }

    protected TMap GetSingle(Expression<Func<TMap, bool>> predicate)
    {
        var efPredicate = AutoMapperConfig.Mapper.MapExpression<Expression<Func<TDb, bool>>>(predicate);
        var efSpOption = DbSet.SingleOrDefault(efPredicate);
        var spOption = AutoMapperConfig.Mapper.Map<TMap>(efSpOption);
        return spOption;
    }

    protected async Task<TMap> GetSingleAsync(Expression<Func<TMap, bool>> predicate)
    {
        var efPredicate = AutoMapperConfig.Mapper.MapExpression<Expression<Func<TDb, bool>>>(predicate);
        var efSpOption = await DbSet.SingleOrDefaultAsync(efPredicate);
        var spOption = AutoMapperConfig.Mapper.Map<TMap>(efSpOption);
        return spOption;
    }

    // ... И ДРУГИЕ МЕТОДЫ РЕПОЗИТОРИЯ
    #endregion


    #region Methode
    private IQueryable<TDb> Include(params Expression<Func<TDb, object>>[] includeProperties)
    {
        IQueryable<TDb> query = DbSet.AsNoTracking();
        return includeProperties.Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
    }
    #endregion


    #region Disposable
    public void Dispose()
    {
        Context?.Dispose();
    }
    #endregion
}

EfDeviceOptionRepository.cs - A SPECIFIC REPOSITORY IMPLEMENTING IDeviceOptionRepository

public class EfExchangeOptionRepository : EfBaseRepository<EfExchangeOption, ExchangeOption>, IExchangeOptionRepository
{
    #region ctor

    public EfExchangeOptionRepository(string connectionString) : base(connectionString)
    {
    }

    #endregion



    #region CRUD

    public new ExchangeOption GetById(int id)
    {
        return base.GetById(id);
    }

    public new async Task<ExchangeOption> GetByIdAsync(int id)
    {
        return await base.GetByIdAsync(id);
    }

    public new ExchangeOption GetSingle(Expression<Func<ExchangeOption, bool>> predicate)
    {
        return base.GetSingle(predicate);
    }

     // ... И ДРУГИЕ МЕТОДЫ РЕПОЗИТОРИЯ (ЕCЛИ protected ДОCТУП В БАЗОВОМ КЛАССЕ ПОМЕНЯТЬ НА public то можно использовать базовую реализацию, не замещая метод через new)

    #endregion
}

The project BL.Services contains various business logic services and one of the services combines work with repositories providing a user-friendly interface.

MediatorForOptions.cs - SOME HIGH-LEVEL LOGIC FOR WORKING WITH REPOSITORIES

/// <summary>
/// Сервис объединяет работу с репозиотриями опций для устройств.
/// DeviceOption + ExchangeOption + TransportOption.
/// </summary>
public class MediatorForOptions
{
    #region fields
    private readonly IDeviceOptionRepository _deviceOptionRep;
    private readonly IExchangeOptionRepository _exchangeOptionRep;
    private readonly ISerialPortOptionRepository _serialPortOptionRep;
    private readonly ITcpIpOptionRepository _tcpIpOptionRep;
    private readonly IHttpOptionRepository _httpOptionRep;
    #endregion


    #region ctor
    public MediatorForOptions(IDeviceOptionRepository deviceOptionRep,
        IExchangeOptionRepository exchangeOptionRep,
        ISerialPortOptionRepository serialPortOptionRep,
        ITcpIpOptionRepository tcpIpOptionRep,
        IHttpOptionRepository httpOptionRep)
    {
        _deviceOptionRep = deviceOptionRep;
        _exchangeOptionRep = exchangeOptionRep;
        _serialPortOptionRep = serialPortOptionRep;
        _tcpIpOptionRep = tcpIpOptionRep;
        _httpOptionRep = httpOptionRep;
    }
    #endregion


    #region Methode
     //МЕТОДЫ ОБЪЕДИНЯЮЩИЕ РАБОТУ С РЕПОЗИТОРИЯМИ 
    #endregion
}

Project WebServer - Application entry point (WebApi) uses Autofac as a DI container.

RepositoryAutofacModule.cs - the module for registering DI dependencies for the repository resolution (selects a specific storage system)

public class RepositoryAutofacModule : Module
    {
        private readonly string _connectionString;


        #region ctor
        public RepositoryAutofacModule(string connectionString)
        {
            _connectionString = connectionString;
        }
        #endregion


        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<EfSerialPortOptionRepository>().As<ISerialPortOptionRepository>()
                .WithParameters(new List<Parameter>
                {
                    new NamedParameter("connectionString", _connectionString),
                })
                .InstancePerLifetimeScope();

            builder.RegisterType<EfTcpIpOptionRepository>().As<ITcpIpOptionRepository>()
                .WithParameters(new List<Parameter>
                {
                    new NamedParameter("connectionString", _connectionString),
                })
                .InstancePerLifetimeScope();

            builder.RegisterType<EfHttpOptionRepository>().As<IHttpOptionRepository>()
                .WithParameters(new List<Parameter>
                {
                    new NamedParameter("connectionString", _connectionString),
                })
                .InstancePerLifetimeScope();

            builder.RegisterType<EfExchangeOptionRepository>().As<IExchangeOptionRepository>()
                .WithParameters(new List<Parameter>
                {
                    new NamedParameter("connectionString", _connectionString),
                })
                .InstancePerLifetimeScope();

            builder.RegisterType<EfDeviceOptionRepository>().As<IDeviceOptionRepository>()
                .WithParameters(new List<Parameter>
                {
                    new NamedParameter("connectionString", _connectionString),
                })
                .InstancePerLifetimeScope();
        }
    }

MediatorsAutofacModule.cs - module for registering DI dependencies for resolving business logic services.

MediatorsAutofacModule.cs   
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<MediatorForOptions>().InstancePerDependency();
    }
}

ALL-------------------------

I.e. I use MediatorForOptions to work with options everywhere on the project.

МИНУСЫ которые вижу я 

1. МНОООГО маппинга - т.к. у каждой системы хранения своя модель данных, но система хранения обязуется работать в общих типах (Entities из DAL.Abstract).

2. Запросы к БД сложно оптимизировать т.к. наружу торчит не IQuereble, а Ienumerable. Следовательно каждый метод репозитория выполняет какое-то 1 действие и их нельзя объединит. (паттерн UnitOfWork не использую).

3. В новой версии 2.1 EfCore появилась система регистрации контекста в ПУЛЕ (services.AddDbContextPool(...)), вместо perScope. Что должно увеличить производительность. Но в моей модели где я контекст создаю сам, эту фишку НЕЛЬЗЯ использовать.

4. Довольно много кода.

Is worth whether to bother at all?

And what can be improved?

Author: A K, 2018-11-06

1 answers

From similar applications on github, RealWorld example app is immediately recalled, although in general there are a lot of variations of the template.

Mark Seeman (Mark Seeman) in his book Dependency injection CSharp shows an option for how to completely get rid of EF in an application-and I somehow, for the sake of curiosity, put together an application that was completely untethered from DAL (a solution in which there were two dal implementations - one on EF, the other on dapper, you could switch in runtime, and the studio showed that the project with exe was not depended on these two). So if you want-you can get rid of it almost completely, there would be a desire.

As for DbSet, you write correctly, because this is a ready-made implementation of the microsoft repository pattern and it is already in EF. Yes, and in books/articles, this is a lot where it slips, plus on so it was discussed in the comments to the questions/answers (you can search for similar things from PashaPash - in my and Bald questions). I once used to prefer IEnumerable and IReadOnlyCollection just for the fact that fewer dependencies on EF. Seeman has a lot of sometimes controversial points in his articles, so it's better to make up your own opinion. Or even so: it is not understood by those who do not particularly like DDD and prefer ready-made libs from Microsoft without going much beyond the standard solutions. I think that with your approach, on the contrary, you will like a lot. And yet-you will soon come to the fact that EF, even for migrations, is not very good, and you will understand the beauty of the Database First approach (which is almost completely under the knife in core)

About mapping. There is no escape from mapping with this approach. I have seen many different implementations of the pattern and there is only one way (wrong, of course) not to do constant mapping - this is when the same classes are used both as domain objects and as DAL objects. However, if you have read Uncle Bob's "Pure Architecture" and understand what architectural layers are and what architectural boundaries look like , then you must understand that either you clearly define the architectural boundary and then - only conversion from one class to another on the border (mapping, either manually writing, or relying on automappers), or erasing this border.

 2
Author: A K, 2018-11-06 07:03:20