我一直使用存储库模式,但更像是单元测试所需的包装器,可以通过装饰器和其他一些贴花功能嵌入缓存,仅此而已,我没有看到任何其他好处。
我对模式经典实现的疑惑和疑问:
不仅在使用数据访问层的项目(特定存储技术的特定存储库)中存在上下文依赖关系,而且在必须在 DI 中注册 Ef 上下文的主项目中也存在上下文依赖关系。以便稍后存储库在构造函数中接收上下文。这不是很奇怪吗?该思想意味着使用抽象存储库作为对数据的通用访问,例如,10 个存储库实现 IGenericDataRepository,每个存储库(项目)都有自己的依赖项和自己的数据存储模型。也就是说,EFCore 上下文只能在实现基于 EFCore 技术的存储系统的项目中使用。与外界的唯一连接是设置和连接字符串。但基本上每个人都只使用这种存储库模式的变体(甚至在 EFCore 场外给出了一个示例)。
一些暴露在外面的 IQuerible 而不是 Ienumerable ——那何必费心呢,DbSet 接口已经不错了。
我试图制作最抽象的存储库,可以在 DI 中用各种存储技术实现(通过 EFCore 的 SQL,存储在 XML 文件中,存储在 NoSql 中)来替换。下面我将给出项目的结构和代码,请发表你的意见,T.K. 出现了一个具有复杂数据层的项目,我再次考虑存储库。您可能不必用另一个存储系统替换 EfCoreRepository,但您仍然希望尽可能独立和抽象地处理数据。
项目结构
DAL.Abstract 项目包含:
IGenericDataRepository.cs
- 抽象存储库接口
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
- 特定存储库的接口。突然之间,您需要一种非常具体的方法来处理特定存储库的数据,而不是将其添加到 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>
{
}
在文件夹Entities
中,需要保存在存储库中的数据模型。纯模型(没有属性和附加存储特定设备的属性)
DeviceOption.cs
- 某种可用于业务逻辑的数据模型
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; }
}
该项目DAL.EFCore
包含特定存储技术的实施
进入数据模型文件夹,可以方便地Entities
在其中存储特定技术的数据(在本例中为EFCore
)。
EfDeviceOption.cs
- 相同的模型 ( DeviceOption
),仅适用于存储系统,以它可以理解的形式。
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
- 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
- 用于迁移系统的上下文创建工厂
- 模型与之间的AutoMapperConfig.cs
映射设置DAL.Abstract
DAL.EFCore
Repository
在特定存储库的文件夹实现中
EfBaseRepository.cs
- 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
- 特定的存储库实施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
}
该项目BL.Services
包含各种业务逻辑服务,其中一项服务与存储库集成工作,提供方便的界面。
MediatorForOptions.cs
- 一些与存储库一起使用的高级逻辑
/// <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
}
项目WebServer
- 应用程序入口点 ( WebApi
)Autofac
用作DI
容器。
RepositoryAutofacModule.cs
- 用于注册 DI 依赖项以解析存储库的模块(选择特定的存储系统)
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
- 用于注册 DI 依赖项以解决业务逻辑服务的模块。
MediatorsAutofacModule.cs
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<MediatorForOptions>().InstancePerDependency();
}
}
全部 - - - - - - - - - - - - -
т.е. я использую MediatorForOptions
для работы с опциями везде по проекту.
МИНУСЫ которые вижу я
1. МНОООГО маппинга - т.к. у каждой системы хранения своя модель данных, но система хранения обязуется работать в общих типах (Entities из DAL.Abstract).
2. Запросы к БД сложно оптимизировать т.к. наружу торчит не IQuereble, а Ienumerable. Следовательно каждый метод репозитория выполняет какое-то 1 действие и их нельзя объединит. (паттерн UnitOfWork не использую).
3. В новой версии 2.1 EfCore появилась система регистрации контекста в ПУЛЕ (services.AddDbContextPool(...)), вместо perScope. Что должно увеличить производительность. Но в моей модели где я контекст создаю сам, эту фишку НЕЛЬЗЯ использовать.
4. Довольно много кода.
Стоит ли вообще заморачиваться?
И что можно улучшить?
Из подобных приложений на гитхабе сразу вспоминается RealWorld example app, хотя вообще вариаций шаблона масса.
У Марка Симана ( Mark Seeman) в его книге Dependency injection CSharp показан вариант, как в приложении совсем отвязаться от EF - и я как-то ради любопытства собрал приложение, полностью отвязанное от DAL (солюшен, в котором было две реализации dal - одна на EF, другая на dapper, переключаться можно было в рантайме, а студия показывала что проект с exe не зависел от этих двух). Так что если хотите - отвязаться можно практически полностью, было бы желание.
По поводу DbSet вы правильно пишете, это ведь уже готовая реализация паттерна репозиторий от майкрософт и он уже есть в EF. Да и в книгах/статях такое много где проскальзывает, плюс на so обсуждалось в комментариях к вопросам/ответам (можете поискать у PashaPash подобное - в моих и Bald вопросах). Я когда-то раньше предпочитал IEnumerable и IReadOnlyCollection только за то, что меньше зависимостей от EF. У Симана в статьях много порой спорных моментов, поэтому лучше составьте своё мнение. Или даже так: его не понимают те, кто не особо любит DDD и предпочитает готовые либы от майкрософт не выходя особо за рамки типовых решений. Я думаю, что с вашим подходом вам наоборот многое понравится. И ещё - вы скоро придёте к тому, что EF даже и для миграций не особо-то и поймёте прелесть подхода Database First (который в core практически полностью под нож пустили)
О маппинге. От маппинга при подобном подходе никуда не деться. Я видел много вариантов реализаций паттерна и есть только один способ (неправильный, разумеется) не делать постоянный маппинг - это когда одни и те же классы используются и как доменные объекты и как объекты DAL. Однако если вы читали "Чистую архитектуру" дядюшки Боба и понимаете, что такое архитектурные слои и как выглядят архитектурные границы -- то должны понимать, что либо вы чётко обозначаете архитектурную границу и тогда - только конвертация из одних классов в другие на границе (маппинг, либо вручную писать, либо на автомапперы полагаться), либо стирание этой границы.