如何将 MS_DESCRIPTION 属性 添加到 M:N 连接 table 中的列?

How to add MS_DESCRIPTION property to column in a M:N join table?

回到我之前的问题:

How do you add column description for a foreign key in another table utilizing EF 6?

和之前的 post:

How to add description to columns in Entity Framework 4.3 code first using migrations?

根据上述参考资料,我如何插入一个扩展的属性(例如MS_Description)用于多对多关系 其中 EF 6 会在后台自动创建连接 table?

仅供参考,更多参考资料供您参考:

Towards the Self-Documenting SQL Server Database

SQL Server extended properties

sp_addextendedproperty (Transact-SQL)

好的,是的,可以通过迁移实现。这绝对是对 EF 工作原理的一次有趣探索,因为迁移是我倾向于避免的事情。

第一个线索在这里:(https://karatejb.blogspot.com/2018/09/entity-framework-6-code-first-6-add-or.html) 使用 EF 读写 MS_Description。从那里开始,这是一个为迁移启用它的案例。

对于包含两个实体 FidgetSpinnerDbContext,其中每个实体都是多对多,我想要一个 FidgetSpinner table 由 EF 生成:

[Table("Fidgets")]
public class Fidget
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int FidgeId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Spinner> Spinners { get; set; } = new List<Spinner>();
}

[Table("Spinners")]
public class Spinner
{
    [Key]
    public int SpinnerId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Fidget> Fidgets { get; set; } = new List<Fidget>();
}

然后映射到我的FidgetSpinner table:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Fidget>()
            .HasMany(x => x.Spinners)
            .WithMany(x => x.Fidgets)
            .Map(x =>
            {
                x.MapLeftKey("FidgetId"); 
                x.MapRightKey("SpinnerId"); 
                x.ToTable("FidgetSpinners"); 
            });
    }

非常简单,但我们想将描述添加到此生成的 table 上的 FidgetId 和 SpinnerId 列。有效的解决方案是利用 Table 注释和迁移:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        string descriptions = JsonConvert.SerializeObject(new[]
        {
            new KeyValuePair<string, string>("SpinnerId", "FK Reference to a Spinner"),
            new KeyValuePair<string, string>("FidgetId", "FK Reference to a Fidget")
        });

        modelBuilder.Entity<Fidget>()
            .HasMany(x => x.Spinners)
            .WithMany(x => x.Fidgets)
            .Map(x =>
            {
                x.MapLeftKey("FidgetId"); 
                x.MapRightKey("SpinnerId"); 
                x.ToTable("FidgetSpinners"); 
                x.HasTableAnnotation("Descriptions", descriptions);
            });
    }

在这里,我将我的注释附加为 JSON 字符串,其中包含用于我要添加的列名称和描述的 KeyValuePairs。这可以很容易地成为自定义类型的容器。但是,无论您发送什么,都必须可序列化为字符串。

接下来将创建一个 SQL 生成器,它将响应创建 Table 操作并检查我们的描述 Table 注释,然后对于任何匹配的列,附加他们的使用 sp_addextendedproperty:

的描述
public class DescriptionMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(CreateTableOperation createTableOperation)
    {
        base.Generate(createTableOperation);
        if (createTableOperation.Annotations.TryGetValue("Descriptions", out object descriptionData))
        {
            try
            {
                var descriptionValues = JsonConvert.DeserializeObject<KeyValuePair<string, string>[]>(descriptionData.ToString());

                foreach (var descriptionValue in descriptionValues)
                {
                    var column = createTableOperation.Columns.SingleOrDefault(x => x.Name.ToLower() == descriptionValue.Key.ToLower());
                    if (column != null)
                    {
                        var tableNameParts = createTableOperation.Name.Split('.');
                        var schemaName = tableNameParts.First();
                        var tableName = tableNameParts.Last();
                        var statement = string.Format(@"EXEC sp_addextendedproperty @name=N'MS_Description', @value = N'{0}', @level0type=N'Schema', @level0name= {1}, @level1type=N'Table', @level1name={2}, @level2type=N'Column', @level2name={3}",
                            descriptionValue.Value, schemaName, tableName, column.Name);
                        Statement(statement);
                    }
                }
            }
            catch { } // TODO: Handle situation where invalid description annotation provided. (I.e. not provided collection of key value pairs...
        }
    }
}

这只是一个模板,应该扩展异常处理,例如处理无效值或请求不存在的列的情况。 (目前被忽略)createTableOperation.Name 包含架构名称,因此我们将其拆分以获得存储过程调用的架构和 table 名称(真的很粗糙,建议对此进行一些更好的检查)。

最后一步是使用迁移配置注册此 SQL 生成器:

internal sealed class Configuration : DbMigrationsConfiguration<TestDbContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
        SetSqlGenerator("System.Data.SqlClient", new DescriptionMigrationSqlGenerator());
    }
}

这假定自动生成,或者您可以将其设置为手动生成。

现在,当迁移创建我们用描述注释的连接 table 时,我们请求的描述将添加到数据库列中。

感谢Steve Py's answer, it gave me the idea to create my own attributes and to combine with the previous solution from this question

以前的解决方案适用于 1:1 并且需要完全定义的关系(参见 here1:n[=33= 】 关系。由于 EF 6 自动为 m:n 关系创建 join-table,该解决方案将引发异常,说明外键列不存在,因为它正在寻找错误 table.

我在我这边实施的解决方案是给定解决方案的另一种变体,它使用自定义属性来定义描述,还额外需要定义外键并为 加入 table m:n 关系。

我相信那里有更优雅的答案,但在它发布之前,我正在实现下面的代码。

以下是我创建的自定义属性:

public class CustomTblDesc : Attribute
{
    private string desc = "";

    public CustomTblDesc(string description)
    {
        desc = description;
    }

    public string Description { get { return desc; } }
}

public class CustomColDesc : Attribute
{
    private string desc = "";
    private string table = "";
    private string propName = "";

    public CustomColDesc(string description)
    {
        desc = description;
    }

    public CustomColDesc(string description, string tableName)
    {
        desc = description;
        table = tableName;
    }

    public CustomColDesc(string description, string tableName, string linkedTablePropertyName)
    {
        desc = description;
        table = tableName;
        propName = linkedTablePropertyName;
    }

    public string Description { get { return desc; } }
    public string LinkedTable { get { return table; } }
    public string LinkedTablePropertyName { get { return propName; }}
}

下面是我根据这个 question 的解决方案修改的代码:

private void SetTableDescriptions(Type tableType) { 字符串 table名称 = tableType.Name;

    // -- Set table description

    CustomTblDesc tblDesc = null;

    var custDescs = tableType.GetCustomAttributes(typeof(CustomTblDesc), false);
    if (custDescs != null && custDescs.Length > 0)
    {
        tblDesc = custDescs[0] as CustomTblDesc;
        SetTableDescription(tableName, tblDesc.Description);
    }

    // -- Set column description

    foreach (var prop in tableType.GetProperties(System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
    {
        if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string)) 
            continue;
        else
        {
            CustomColDesc colDesc = null;

            custDescs = prop.GetCustomAttributes(typeof(CustomColDesc), false);
            if (custDescs != null && custDescs.Length > 0)
            {
                colDesc = custDescs[0] as CustomColDesc;
             
                if (string.IsNullOrEmpty(colDesc.LinkedTable))
                    SetColumnDescription(tableName, prop.Name, colDesc.Description);
                else
                    SetColumnDescription(colDesc.LinkedTable, colDesc.LinkedTablePropertyName, colDesc.Description);
            }
        }
    }
}