繁体   English   中英

是否有更好的方法在 EF Core 中包含其他字段/列而不加载整个导航属性

[英]Is there a better way to include additional fields/columns in EF Core without loading the whole navigation property

是否有更好的方法在 EF Core 中包含其他字段/列而不加载整个导航属性

我有两个实体说员工和部门。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }

    [JsonIgnore]
    public Department Department { get; set; }
}

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }

    // ... more fields.
}

我需要将员工列表返回给客户端,但我不想包含仅用于服务器端进程的整个 Department 实体,但DepartmentIdDepartmentName除外。

我有两个解决方案。

解决方案1是使用计算属性和导航属性。

public class Employee
{
    // ...
    
    public string DepartmentName
    {
        get
        {
            if (Department != null)
            {
                return Department.Name;
            }
            else
            {
                return null;
            }
        }
    }
}

// Query for department name but also have to include all fields.
public static List<Employee> GetEmployees(MyDbContext context)
{
    return context.Employees.Include(e => e.Department).ToList();
}

解决方案2是使用动态加入。

public class Employee
{
    // ...
    
    [NotMapped]
    public string DepartmentName { get; set; }
}

// Query for department name without navigation property.
public static List<Employee> GetEmployees(MyDbContext context)
{
    Func<Employee, Department, Employee> map = (em, dept) =>
    {
        em.DepartmentName = dept.Name;
        return em;
    };
    var query = from em in context.Employees
                join dept in context.Departments on em.DepartmentId equals dept.Id
                select map(em, dept);
    return query.ToList();
}

但是如果要包含更多的外部字段,则 linq 查询将很乏味。 另一个问题是,这不是任意实体的通用方式,而且我有更多的实体可以像上面一样实现。

我想知道是否有任何其他优雅的实现或官方方式。

对于在 DbContext 范围内运行的方法,通过传递 DbContext 实例似乎是您的情况,我建议让它们返回IQueryable<TEntity>而不是List<TEntity> 通过这种方式,消费者可以在他们认为合适的时候进一步细化查询。 这包括将实体结构投影到需要的地方,(只需要一个 DepartmentName)处理排序、分页等事情。这在开发存储库模式时很常见。

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual Department Department { get; set; }
}

public class Department
{
    public int Id { get; set; }
    public string Name { get; set; }

    // ... more fields.
}

// Then somewhere in a Repository class...

public IQueryable<Employee> GetEmployees(MyDbContext context)
{
    return context.Employees.AsQueryable();
}

通常出现的第一个问题是“有什么意义?为什么不直接使用 DbContext?”。 实现这样的存储库模式可能有两个很好的理由。 首先启用单元测试。 模拟 DbContext 是“混乱的”,因为模拟具有返回IQueryable<Employee>方法的依赖项很容易。 另一个原因是系统通常需要执行低级规则,例如使用软删除(即IsActive=false而不是删除行)或多租户的系统。 (即访问系统的多个 ClientId 的员工)存储库可以作为极好的边界来确保这些低级规则得到执行。

public static IQueryable<Employee> GetEmployees(MyDbContext context, bool includeInactive = false)
{
    var clientId = ClientService.ResolveCurrentUserClient();
    var query context.Employees.Where(x => x.ClientId == clientId);
    if(!includeInactive)
        query = query.Where(x => x.IsActive);
    return query;
}

在上面的示例中,ClientService 将是一个依赖项,它被配置为检查当前用户及其与租户客户端的关联以过滤数据,然后选择性地仅过滤掉活动行。

如果您不打算实施单元测试并且没有要强制执行的低级规则,那么我的建议是只使用 DbContext 而不是添加抽象层。

对于返回 DbContext 范围之外的数据的方法,我推荐的最佳解决方案是使用投影返回数据对象(视图模型或 DTO)而不是实体。 视图模型只能表示您的使用者需要的数据,您可以利用Select或 Automapper 的ProjectTo直接从IQueryable填充它,从而实现最小大小的有效负载和最大性能。

[Serializable]
public class EmployeeViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int DepartmentId { get; set; }
    public string DepartmentName { get; set; }

    public static MapperConfigurationExpression BuildMapExpression(MapperConfigurationExpression expression = null)
    {
        if (expression == null)
            expression = new MapperConfigurationExpression();

        expression.CreateMap<Employee, EmployeeViewModel>()
            .ForMember(x => x.DepartmentId, opt => opt.MapFrom(src => src.Department.Id))
            .ForMember(x => x.DepartmentName, opt => opt.MapFrom(src => src.Department.Name));
        return expression;
    }
}

然后在控制器操作或其他将序列化我们的员工和部门信息的方法中:

public ViewResult List()
{
    using(var context = new MyDbContext())
    {
        var config = new MapperConfiguration(EmployeeViewModel.BuildMapExpression());
        var employees = EmployeeRepository.GetEmployees(context)
            .ProjectTo<EmployeeViewModel>(config)
            .ToList(); // Consider pagination /w Skip/Take
        return View(employees);
    }
}

因此,与其序列化 Employee 实体并担心序列化程序延迟加载数据或有一堆#null 引用,不如将数据打包的方法将生成的IQueryable投影到可安全序列化的 DTO 或 ViewModel 类中。 这为性能和内存使用产生了高效的查询。 我解释了为什么要避免发送超出其 DBContext 范围的实体,以及为什么在我对这个问题的回答中应该始终认为它们是完整的或完整的:
EntityState.Deleted 和 Remove() 方法之间的真正区别是什么? 什么时候使用它们?

如果我只需要加载某些属性,我通常会采用这种方式。 添加到员工类额外属性 DepartmentName 并添加属性 [NotMaped](或 Ignore in fluent)

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    .......

    [NotMapped]
    public string DepartmentName { get; set; }
}

行动

public static List<Employee> GetEmployees(MyDbContext context)
{
    return context.Employees
                      .Select(e=> new Employee
                       {
                         Id=e.Id,
                         Name=e.Name,
                         DepartmentName=e.Department.Name 
                        }).ToList();
}

我不喜欢使用映射器,它非常有限,所以如果我需要包含很多属性,我通常会这样做

public static List<Employee> GetEmployees(MyDbContext context)
{
  var employees= context.Employees
                      .Include(i=> Department)
                      .ToList();

   foreach(var item in employees)
  { 
           item.DepartmentName=item.Department.Name 
           .....
           ..... another calculated fiels if needed

           item.Department=null;
      }
    return employees;
}

暂无
暂无

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

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