实体框架核心计算属性多个表

时间:2020-04-24 18:03:42

标签: c# sql-server asp.net-core entity-framework-core azure-sql-database

我正在使用Entity Framework Core 3.1开发ASP.Net Core Web应用程序。 假设我有这四个实体:

public class Project
{
    public string Name { get; set; }

    public DateTimeOffset CreationDate { get; set; }

    public int ProjectTypeId { get; set; }
    public ProjectType ProjectType { get; set; }

    public ICollection<ProjectScope> ProjectScopes { get; private set; } = new HashSet<ProjectScope>();
}

public class ProjectScope
{
    public int ProjectId { get; set; }
    public Project Project { get; set; }

    public int ScopeId { get; set; }
    public Scope Scope { get; set; }

}

public class Scope
{
    public int Id { get; set; }

    public string Name { get; set; }

    public ICollection<ProjectScope> ProjectScopes { get; private set; } = new HashSet<ProjectScope>();
}

public class ProjectType
{
    public int Id { get; set; }

    public string Name { get; set; }
}

我有一个需求,我需要在多个地方显示并能够在数据库级别上对项目名称进行排序: 所有范围按名称+ CreationDate +项目类型名称+项目名称排序

这是我可以通过计算属性实现的方法:

    public string FullName => 
        "[" + ProjectScopes.Select(ps => ps.Scope.Name).OrderBy(s => s).Aggregate((a, b) => a + " - " + b) + "]" +
        " " + CreationDate.ToString("d") +
        " " + ProjectType.Name +
        " " + Name;

当然,它不能由ef core翻译。 我在应用程序的多个数组中显示此属性,并且需要能够根据该字段对结果进行排序。

对我而言,拥有此计算字段的最佳方法是什么?

无论连接到该数据库的哪个客户端,我都必须能够在该字段上进行选择和排序。 (例如可以是PowerBI)

  • 数据库中的计算列?
  • 触发器?
  • 用户定义的功能?

感谢您的时间。

4 个答案:

答案 0 :(得分:2)

这取决于数据量,读取/更新的频率以及排序的频率。 您是否考虑过使用SQL View?具有索引,实现等的能力。

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace sqlview
{
    class Program
    {
        static void Main(string[] args)
        {
            using var dbContext = new EmpDbContext();

            // create db table
            dbContext.Database.EnsureCreated();

            // create view with "FullName" column as an aggregate like "First Name - Middle Name - Last Name"
            dbContext.Database.ExecuteSqlRaw("DROP VIEW IF EXISTS View_EmployeeView;");
            dbContext.Database.ExecuteSqlRaw(
                @"CREATE VIEW View_EmployeeView AS 
                        SELECT
                            e.Id,
                            e.FirstName + ' - ' + e.MiddleName + ' - '  + e.LastName as FullName
                        FROM Employees e;
            ");

            // insert record
            dbContext.Add(new Employee() { FirstName = "John", MiddleName = "Jack", LastName = "Sugarcoater" });
            dbContext.SaveChanges();

            // fetch using view
            var employeesFromView = dbContext.EmployeesFromView.ToListAsync().Result;
            foreach (var emp in employeesFromView)
            {
                Console.WriteLine($"Full name: {emp.FullName}");
            }
        }

        class EmpDbContext : DbContext
        {
            public DbSet<Employee> Employees { get; set; }
            public DbSet<EmployeeView> EmployeesFromView { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(
                    @"YOUR CONNECTION STRING");
            }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                // configure view
                modelBuilder
                    .Entity<EmployeeView>(eb =>
                    {
                        eb.HasNoKey();
                        eb.ToView("View_EmployeeView");
                    });
            }
        }

        public class Employee
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string MiddleName { get; set; }
            public string LastName { get; set; }
        }

        public class EmployeeView
        {
            public int Id { get; set; }
            public string FullName { get; set; } // will be computed on the SQL side
        }
    }
}

答案 1 :(得分:1)

您可以将其作为类本身的属性。

只需通过重写DbContext.OnModelCreating并使用EntityTypeBuilder.Ignore方法来通知EF忽略此属性。

答案 2 :(得分:1)

IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project_scope]'), 'IsTable') = 1
   DROP TABLE [dbo].[project_scope]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project]'), 'IsTable') = 1
   DROP TABLE [dbo].[project]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[scope]'), 'IsTable') = 1
   DROP TABLE [dbo].[scope]
GO
IF OBJECTPROPERTY(OBJECT_ID('[dbo].[project_type]'), 'IsTable') = 1
   DROP TABLE [dbo].[project_type]
GO
CREATE TABLE [dbo].[project_type]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL
)
GO
CREATE TABLE [dbo].[scope]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL
)
GO
CREATE TABLE [dbo].[project]
(
   id INT NOT NULL PRIMARY KEY,
   name NVARCHAR(100) NOT NULL,
   creation_date DATETIMEOFFSET,
   project_type_id INT
)
GO
CREATE TABLE [dbo].[project_scope]
(
   project_id INT NOT NULL,
   scope_id INT NOT NULL
)
GO
INSERT INTO [dbo].[project_type]
   (id, name)
VALUES
   (1, 'Type 1')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (1, 'Scope A')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (2, 'Scope B')
GO
INSERT INTO [dbo].[scope]
   (id, name)
VALUES
   (3, 'Scope C')
GO
INSERT INTO [dbo].[project]
   (id, name, creation_date, project_type_id)
VALUES
   (1, 'Project Avengers', SYSDATETIMEOFFSET(), 1)
GO
INSERT INTO [dbo].[project_scope]
   (project_id, scope_id)
VALUES
   (1, 1),
   (1, 2),
   (1, 3)
GO
IF OBJECT_ID('[dbo].[udfGetProjectFullName]', 'FN') IS NOT NULL
   DROP FUNCTION udfGetProjectFullName
GO
CREATE FUNCTION [dbo].[udfGetProjectFullName](@project_id INT)
RETURNS NVARCHAR(255)
AS
BEGIN
   DECLARE @fullname NVARCHAR(255)

   SELECT @fullname = CONCAT('[', STRING_AGG(s.name, ' - ') WITHIN GROUP (ORDER BY s.name), '] ', p.creation_date, ' ', pt.name, ' ', p.name)
   FROM project AS p
      INNER JOIN project_type AS pt on pt.id = p.project_type_id
      LEFT JOIN project_scope AS ps on ps.project_id = p.id
      LEFT JOIN scope AS s on s.id = ps.scope_id
   WHERE p.id = 1
   GROUP BY p.id, p.name, p.creation_date, pt.name

   RETURN @fullname
END
GO
SELECT id, dbo.udfGetProjectFullName(id) as [full_name]
FROM project
GO

答案 3 :(得分:-2)

您可以使用可以封装此视图的ViewModel,并且该模型可以具有可以评估以上内容的属性。

在我的用例中,我使用了AutoMapper,并创建了一个映射配置,我也在其中进行了转换。就像我正在确定的那样,我需要基于Db的EndDate道具在UI上需要用户的Active属性。

使用自动映射器,我实现了这一点:

public static IEnumerable<Models.ViewModels.User> AsViewModel(this IEnumerable<Models.DataModels.User> userData)
{
        var config = new MapperConfiguration(cfg => {                
            cfg.CreateMap<Models.DataModels.User, Models.ViewModels.User>()
            .ForMember(vm => vm.Active, d => d.MapFrom(m => !m.EndDate.HasValue))
            .ReverseMap();
        });
        var mapper = config.CreateMapper();
        return mapper.Map<IEnumerable<Models.DataModels.User>, IEnumerable<Models.ViewModels.User>>(userData);
}

通过EF与Db交互的我的数据模型是:

namespace PM.Models.DataModels
{
    [Table("Users")]
    public class User
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        [Required]
        public string FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
        [Required]
        public string UserId { get; set; }
        [DefaultValue("getdate()")]
        public DateTime Created
        {
            get { return _createdValue == DateTime.MinValue ? DateTime.Now : _createdValue; }
            set { _createdValue = value; }
        }
        private DateTime _createdValue;

        public DateTime? EndDate { get; set; }
    }
}

和我用于与表示层进行交互的Viewmodel:

namespace PM.Models.ViewModels
{
    public class User
    {
        public Guid Id { get; set; }
        [Required]
        public string FirstName { get; set; }
        //[Required]
        public string LastName { get; set; }
        public string FullName { get { return string.Format($"{LastName}, {FirstName}"); } }
        [Required]
        public string UserId { get; set; }
        public bool Active { get; private set; }
    }
}

就像指出的其他答案一样,您可以简单地使用模型来封装您的全名,或者在扩展的业务用例中,可以转换为复杂的评估。