修改现有的通用存储库模式以添加选择

mik*_*ajs 1 c# linq entity-framework-core

我想修改现有的通用存储库以添加可选select功能,例如 Entity Framework Core 中的功能。

期望的结果:

private readonly IUnitOfWork _unit;
// ...

// without using select functionality
IEnumerable<Entity> entities = await _unit.Account.AllAsync();
Entity? x = await _unit.Account.SingleAsync(x => x == id);

// using select functionality
IEnumerable<DTO> y = await _unit.Account.AllAsync(select: x => new DTO
{
    Name = x.Name
});
DTO? y = await _unit.Account.SingleAsync(x => x == id, select: x => new DTO
{
    Name = x.Name
});
Run Code Online (Sandbox Code Playgroud)

我尝试实现此问题的解决方案Select certain columns in a genericrepository function但参数是必需的,我希望它是可选的。

为简单起见,我只在通用存储库中保留要添加此功能的方法:

IBaseRepository.cs

public interface IBaseRepository<T> where T : BaseEntity
{
    Task<IEnumerable<T>> AllAsync(
        Expression<Func<T, bool>>? filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>>? order = null, 
        Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
        int skip = 0,
        int take = int.MaxValue,
        Track track = Track.NoTracking);

    Task<T?> SingleAsync(
        Expression<Func<T, bool>> filter, Func<IQueryable<T>, 
        IIncludableQueryable<T, object>>? include = null,
        Track track = Track.Tracking);
}
Run Code Online (Sandbox Code Playgroud)

BaseRepository.cs

public class BaseRepository<T> : IBaseRepository<T> where T : BaseEntity
{
    private readonly DataContext _context;
    internal DbSet<T> _set;

    public BaseRepository(DataContext context)
    {
        _context = context;
        _set = context.Set<T>();
    }

    public async Task<IEnumerable<T>> AllAsync(
        Expression<Func<T, bool>>? filter = null, 
        Func<IQueryable<T>, IOrderedQueryable<T>>? order = null, 
        Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
        int skip = 0, int take = int.MaxValue, Track track = Track.NoTracking)
    {
        IQueryable<T> query = _set;
        switch (track)
        {
            case Track.NoTracking:
                query = query.AsNoTracking();
                break;
            case Track.NoTrackingWithIdentityResolution:
                query = query.AsNoTrackingWithIdentityResolution();
                break;
            default:
                query = query.AsTracking();
                break;
        }
        query = skip == 0 ? query.Take(take) : query.Skip(skip).Take(take);
        query = filter is null ? query : query.Where(filter);
        query = order is null ? query : order(query);
        query = include is null ? query : include(query);
        return await query.ToListAsync();
    }

    public async Task<T?> SingleAsync(
        Expression<Func<T, bool>> filter,
        Func<IQueryable<T>, IIncludableQueryable<T, object>>? include = null,
        Track track = Track.Tracking)
    {

        IQueryable<T> query = _set;
        switch (track)
        {
            case Track.NoTracking:
                query = query.AsNoTracking();
                break;
            case Track.NoTrackingWithIdentityResolution:
                query = query.AsNoTrackingWithIdentityResolution();
                break;
            default:
                query = query.AsTracking();
                break;
        }
        query = filter is null ? query : query.Where(filter);
        query = include is null ? query : include(query);
        return await query.SingleOrDefaultAsync();
    }
}
Run Code Online (Sandbox Code Playgroud)

in BaseEntity.cs(每个类 dbtable 都会继承自BaseEntity

public abstract class BaseEntity
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; set; } = DateTime.Now;
    public DateTime? UpdatedAt { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_* Py 10

诚实地问自己:“为什么我认为我需要通用存储库?”

答:“因为我想在 EF 上添加一个抽象层,这样我就可以在需要时替换它。”

  • 你是自我实现预言的受害者;限制您自己和您的应用程序充分利用 EF 的能力。该解决方案要么非常有限,要么非常复杂且存在问题,以至于 EF 将被认为太慢或无法完成所需的操作,并且需要用其他人说服您更好/更快的解决方案来替换。

答:“因为我不想让领域知识或 EF 特定知识污染我的业务逻辑。”

  • 这是个谎言。将表达式传递到存储库会因领域和 EF 特定的知识和限制而污染领域。如果表达式传递给 EF 的 Linq 方法,它们必须符合域并且必须适合 EF。您删除了对 EF 的引用,但仍然对您的消费者施加 EF 的限制。您不能引用表达式中的方法,也不能使用未映射的属性。EF 和您选择的提供程序需要能够将这些表达式转换为 SQL,否则您将需要编写更多代码来引入您自己的表达式解析器和适配器。

答:“因为我希望能够引入一个抽象点,这样我就可以更轻松地编写单元测试。”

  • 这是一个很好的理由,但可以用一种更简单的方法来解决:

IQueryable<T> All();
IQueryable<T> Single(int id);
Run Code Online (Sandbox Code Playgroud)

就是这样。您的消费者可以支持过滤、排序、分页、存在检查、计数、投影,甚至可以自行确定操作的开销是否async是必要的。模拟起来非常简单,只需增加一点复杂性即可支持传回数据async使用。存储库方法可以强加基本级别的过滤,例如IsActive软删除系统,或检查当前用户的授权并将其纳入返回的结果中。

当您想要引入一个解决方案来满足“DNRY”(不要重复自己)或在您的应用程序中强制执行一定程度的一致性时,或者在相同的情况下(不仅仅是相似)的关注点(例如 Web API 服务和 Web 应用程序)之间,可以通过移交给一个公共服务来完成,该服务使用存储库以一致的方式获取和打包数据以返回 DTO。否则,像对待 MVC 控制器一样对待存储库,将其范围限定为对一个使用者负责,并且只有一个理由进行更改。

反对退货的典型论点IQueryable是,这是一个薄弱或有漏洞的抽象,并且给了消费者一把上膛的猎枪。这是 100% 真实的,它是一个弱抽象,这使得它很容易被模拟。如果您不需要模拟它进行单元测试,那么您只是“不需要它”。就为开发人员提供一个装载武器而言,恕我直言,对于一个项目来说,为实施开发人员提供他们需要的工具,使他们能够针对领域编写有效的表达式以及如何正确执行和纠正的培训/知识要好得多。如果发现错误;而不是尝试将技术抽象出来以“简化”,或者从本质上讲,最终会引入同样多的复杂性和制造混乱的能力,只是在界面上使用您的名字而不是 EF。