简体   繁体   English

创建可重用的 function 以根据 EF Core 数据 model 中的相关数据返回值的最佳方法是什么?

[英]What is the best way to create a reusable function to return a value based on related data in an EF Core data model?

I have a standard EF Core data model with several one-many and many-to-many relationships.我有一个标准的 EF Core 数据 model,其中包含几个一对多和多对多的关系。

I want to create a way to produce various data calculations or commonly run procedures.我想创建一种方法来生成各种数据计算或通常运行的程序。 These should return values to be used throughout the application and not values to be stored in the database.这些应该返回要在整个应用程序中使用的值,而不是要存储在数据库中的值。

I'll give a fictional example here of a scenario:我将在这里给出一个虚构的场景示例:

Entity 1 - YearGroup实体 1 - 年YearGroup

Entity 2 - Student实体 2 - Student

Relationship - One YearGroup to many Students关系- YearGroup到许多Students

Now I understand you could simply write in the controller:现在我明白你可以简单地写在 controller 中:

int student = _context.Students.Where(s => s.YearGroupId == ygId).Count()

But let's say I wanted to simplify this by creating a method somewhere which returns data such as this, so I don't have to specify the query every time I get the number of Students in a YearGroup .但是假设我想通过在某处创建一个返回此类数据的方法来简化此操作,这样我就不必每次获取YearGroup中的Students人数时都指定查询。 I appreciate this is a simple example, but others could be more complex.我很欣赏这是一个简单的例子,但其他例子可能更复杂。

I was thinking adding a field to yeargroup.cs model such as:我正在考虑向yeargroup.cs model 添加一个字段,例如:

public int TotalStudents { //code here to get students from students table }

Which I could then use like:然后我可以使用它:

@model.YearGroup.TotalStudents

But I don't know how to get this to include relational data, ie from the students table.但我不知道如何让它包含关系数据,即从学生表中获取。

I would prefer not create random methods in separate, unrelated classes such as GetStudentsInYearGroup(ygId) .我不想在单独的、不相关的类中创建随机方法,例如GetStudentsInYearGroup(ygId) It would be nice to include this in the object model if possible.如果可能的话,最好将其包含在 object model 中。

What would be the best practice way of acheiving this?实现这一目标的最佳实践方法是什么?

Note: I don't have a code editor nor a project setup so what I am going to write might not be compilable.注意:我没有代码编辑器,也没有项目设置,所以我要写的东西可能无法编译。

As simple as your fictional example就像你虚构的例子一样简单

For any case as simple as your fictional example, if you just want to get the total students per a year group, and I assume their relationships are properly setup, you can just simply use the navigation property:对于任何像你虚构的例子一样简单的情况,如果你只想获得每年组的学生总数,并且我假设他们的关系设置正确,你可以简单地使用导航属性:

// Find the target year group
var yearGroup = _context.YearGroups
    .SingleOrDefault(x => x.Id == ygId);

int totalStudents = 0;
if (yearGroup != null)
{
    totalStudents = yearGroup.Students.Count();
}

Extension methods扩展方法

The other way I can think of is to define whatever you need as extension methods of the entity:我能想到的另一种方法是将您需要的任何东西定义为实体的扩展方法:

public static class YearGroupExtensions
{
    public static int GetTotalStudents(this YearGroup yearGroup)
    {
        if (yearGroup == null)
        {
            return 0;
        }

        return yearGroup.Students.Count();
    }

    public static int GetTotalStudents(this YearGroup yearGroup, Gender gender)
    {
        if (yearGroup == null)
        {
            return 0;
        }

        if (gender == null)
        {
            return yearGroup.GetTotalStudents();
        }

        return yearGroup
            .Students
            .Count(x => x.Gender == gender);
    }
}

// usage
// YearGroup yearGroup = GetYearGroup(ygId);
// int totalStudents = yearGroup.GetTotalStudents();

Repository Pattern存储库模式

If you find yourself repeating similar methods you need for most of your entities, it might be good to define a generic repository for them.如果您发现自己在为大多数实体重复您需要的类似方法,那么为它们定义一个通用存储库可能会很好。

I am not here arguing whether this is just a wrapper of DbContext as that itself is already using repository pattern.我不是在这里争论这是否只是DbContext的包装器,因为它本身已经在使用存储库模式。

public interface IEntity { }

public interface IRepository<T> where T : IEntity
{
    IEnumerable<T> GetAll();
    T GetById(int id);
    void Insert(T entity);
    ...
}

public abstract class RepositoryBase<T> : IRepository<T> where T : IEntity
{
    protected readonly AppDbContext _dbContext;

    protected DbSet<T> _entities;

    private RepositoryBase(AppDbContext dbContext)
    {
        _dbContext = dbContext;
        _entities = dbContext.Set<T>();
    }

    public virtual IEnumerable<T> GetAll()
    {
        return _entities.AsEnumerable();
    }

    ...
}

public class YearGroupRepository : RepositoryBase<YearGroup>
{
    ...
}

Separate Persistence and Domain Model分离持久性和域 Model

This is my preference just because I'm a DDD guy, and I want to build anything from the domain first (what businesss problems you're trying to solve), without thinking its backend persistence.这是我的偏好,因为我是一个DDD人,我想首先从域构建任何东西(你试图解决什么业务问题),而不考虑它的后端持久性。

The basic idea here is to have 2 sets of models.这里的基本思想是有 2 套模型。 They could be similar, or totally different.它们可以相似,也可以完全不同。 1 set of models is called the domain model, which reflects your business domain. 1 套模型称为域 model,它反映了您的业务域。 The other set of models is called the persistence model. That could be your regular Entity Framework Entities.另一组模型称为持久性 model。这可能是您的常规实体框架实体。

More on this: https://stackoverflow.com/a/14042539/2410655更多相关信息: https://stackoverflow.com/a/14042539/2410655

Given a DbContext, an entity and a navigation property, you can construct an IQueryable as follows;给定一个 DbContext、一个实体和一个导航属性,您可以按如下方式构造一个 IQueryable;

public static IQueryable AsQueryable(DbContext context, object entity, string navigation){
    var entry = context.Entry(entity);
    if (entry.State == EntityState.Detatched)
        return null;
    var nav = entry.Navigation(navigation);
    return nav.Query();
}

I feel like there should be an existing method for this, but I can't seem to find one right now.我觉得应该有一个现有的方法,但我现在似乎找不到。

You should then be able to use this method in a fairly generic way for any navigation property, without needing to replicate the foreign key criteria all over the place.然后,您应该能够以相当通用的方式对任何导航属性使用此方法,而无需在各处复制外键条件。

public int TotalStudents(DbContext context) =>
    AsQueryable(context, this, nameof(Students))?.Count() ?? 0;

While this would add some minor performance overhead, you could extract the base entity and navigation property from a LamdaExpression, and write some extension methods;虽然这会增加一些小的性能开销,但您可以从 LamdaExpression 中提取基本实体和导航属性,并编写一些扩展方法;

public class QueryableVisitor : ExpressionVisitor
{
    private object obj;
    public object BaseObject { get; private set; }
    public string Navigation { get; private set; }
            
    protected override Expression VisitConstant(ConstantExpression node)
    {
        BaseObject = obj = node.Value;
        return base.VisitConstant(node);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        Visit(node.Expression);
        BaseObject = obj;
        if (node.Member is PropertyInfo prop)
            obj = prop.GetValue(obj);
        else if (node.Member is FieldInfo field)
            obj = field.GetValue(obj);
        Navigation = node.Member.Name;
        return node;
    }
}

public static IQueryable<T> AsQueryable<T>(this DbContext context, Expression<Func<IEnumerable<T>>> expression)
{
    var visitor = new QueryableVisitor();
    visitor.Visit(expression);
    var query = AsQueryable(context, visitor.BaseObject, visitor.Navigation);
    return (IQueryable<T>)query;
}

public static int Count<T>(this DbContext context, Expression<Func<IEnumerable<T>>> expression) =>
    AsQueryable(context, expression)?.Count() ?? 0;

Enabling strongly typed usage like the following;启用如下所示的强类型用法;

public int TotalStudents(DbContext context) =>
    context.Count(() => this.Students);

public int ActiveStudents(DbContext context) =>
    context.AsQueryable(() => this.Students)?.Where(s => s.Active).Count() ?? 0;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM