Ok, yes it is possible through migrations. This was definitely an interesting dive into EF's workings as Migrations are something I tend to avoid.
The first clue was here: (https://karatejb.blogspot.com/2018/09/entity-framework-6-code-first-6-add-or.html) using EF to read and write the MS_Description. From there it is a case of enabling this for migrations.
For a DbContext
containing two entities, Fidget
and Spinner
, where each is a many-to-many where I want a FidgetSpinner
table generated by 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>();
}
Then the mapping to nominate my 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");
});
}
Pretty straight forward, but we want to add Descriptions to the FidgetId and SpinnerId columns on this generated table. The solution that worked was to leverage Table Annotations and Migrations:
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);
});
}
Here I'm appending my annotations as a JSON string containing KeyValuePairs for the column name and description I want to add. This could just as easily be a custom type container. Whatever you send has to be serializable to a string however.
The next thing will be to create an SQL Generator that will respond to Create Table operations and check for our Description Table annotations, then for any matching columns, append their descriptions using 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...
}
}
}
This is a template only and should extend exception handling and such for handling invalid values or cases where a non-existent column is requested. (currently ignored) The createTableOperation.Name includes the schema name so we split that to get the schema and table names for the stored procedure call (Really rough, some better checking is advisable for this).
The last step is to register this SQL generator with a migrations configuration:
internal sealed class Configuration : DbMigrationsConfiguration<TestDbContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
SetSqlGenerator("System.Data.SqlClient", new DescriptionMigrationSqlGenerator());
}
}
This assumes an automatic generation, alternatively you can set it up for manual generation.
Now when the migrations create the joining table that we annotated with the descriptions, the descriptions we requested are added to the DB columns.