C# 实体框架迁移中必填字段的默认值?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/12462100/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Default value for Required fields in Entity Framework migrations?
提问by Andriy Drozdyuk
I've added the [Required]data annotation to one of my models in an ASP.NET MVC application. After creating a migration, running the Update-Databasecommand results in the following error:
我已将[Required]数据注释添加到ASP.NET MVC 应用程序中的模型之一。创建迁移后,运行该Update-Database命令会导致以下错误:
Cannot insert the value NULL into column 'Director', table 'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies'; column does not allow nulls. UPDATE fails. The statement has been terminated.
无法将值 NULL 插入列 'Director',表 'MOVIES_cf7bad808fa94f89afa2e5dae1161e78.dbo.Movies';列不允许空值。更新失败。该语句已终止。
This is due to some records having NULL in their Directorcolumns. How can I automatically change those values to some default (say "John Doe") director?
这是由于某些记录在其Director列中具有 NULL 。如何自动将这些值更改为某个默认值(比如“John Doe”)导演?
Here is my model:
这是我的模型:
public class Movie
{
public int ID { get; set; }
[Required]
public string Title { get; set; }
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Required]
public string Genre { get; set; }
[Range(1,100)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
[StringLength(5)]
public string Rating { get; set; }
[Required] /// <--- NEW
public string Director { get; set; }
}
and here is my latest migration:
这是我最新的迁移:
public partial class AddDataAnnotationsMig : DbMigration
{
public override void Up()
{
AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false));
AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false));
AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false));
}
public override void Down()
{
AlterColumn("dbo.Movies", "Director", c => c.String());
AlterColumn("dbo.Movies", "Rating", c => c.String());
AlterColumn("dbo.Movies", "Genre", c => c.String());
AlterColumn("dbo.Movies", "Title", c => c.String());
}
}
采纳答案by webdeveloper
If I remember correctly, something like this should work:
如果我没记错的话,这样的事情应该有效:
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, defaultValueSql: "'John Doe'"));
Note: The defaultValueSql parameter value is treated as a verbatim SQL statement, so if the required value is effectively a string, like the John Doe example, then single quotes are required around the value.
注意:defaultValueSql 参数值被视为逐字 SQL 语句,因此如果所需的值实际上是一个字符串,如 John Doe 示例,则该值需要单引号。
回答by Pushpendra
public partial class AddDataAnnotationsMig : DbMigration
{
public override void Up()
{
AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
AlterColumn("dbo.Movies", "Genre", c => c.String(nullable: false,defaultValue:"Genre"));
AlterColumn("dbo.Movies", "Rating", c => c.String(maxLength: 5));
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false,defaultValue:"Director"));
}
public override void Down()
{
AlterColumn("dbo.Movies", "Director", c => c.String());
AlterColumn("dbo.Movies", "Rating", c => c.String());
AlterColumn("dbo.Movies", "Genre", c => c.String());
AlterColumn("dbo.Movies", "Title", c => c.String());
}
}
回答by Iravanchi
In addition to the answer from @webdeveloper and @Pushpendra, you need to manually add updates to your migration to update existing rows. For example:
除了@webdeveloper 和@Pushpendra 的回答之外,您还需要手动向迁移添加更新以更新现有行。例如:
public override void Up()
{
Sql("UPDATE [dbo].[Movies] SET Title = 'No Title' WHERE Title IS NULL");
AlterColumn("dbo.Movies", "Title", c => c.String(nullable: false,defaultValue:"MyTitle"));
}
This is because AlterColumnproduces DDL to set the default of the column to some specific value in the table specification. The DDL does not affect existing rows in the database.
这是因为AlterColumn生成 DDL 以将列的默认值设置为表规范中的某个特定值。DDL 不会影响数据库中的现有行。
You're actually making two changes at the same time (setting the default and making the column NOT NULL) and each of them is valid individually, but since you're making the two at the same time, you can expect the system to 'intelligently' realize your intent and set all NULLvalues to the default value, but this is not what's expected all the time.
您实际上是在同时进行两次更改(设置默认值并使列非空)并且每个更改都单独有效,但是由于您同时进行了两次更改,因此您可以期望系统智能地'实现您的意图并将所有NULL值设置为默认值,但这并不是一直期望的。
Suppose you're only setting the default value for the column, and not making it NOT NULL. You obviously don't expect all the NULL records to be updated with the default you provide.
假设您只是为该列设置默认值,而不是将其设为 NOT NULL。您显然不希望使用您提供的默认值更新所有 NULL 记录。
So, in my opinion, this is not a bug, and I don't want EF to update my data in the ways that I don't explicitly tell it to do. The developer is responsible to instruct the system about what to do with the data.
所以,在我看来,这不是一个错误,我不希望 EF 以我没有明确告诉它的方式更新我的数据。开发人员负责指示系统如何处理数据。
回答by workabyte
not sure if this option was always around but just ran into a similar issue, found that i was able to set the default value without running any manual updates using the following
不确定此选项是否始终存在,但只是遇到了类似的问题,发现我能够使用以下方法设置默认值而无需运行任何手动更新
defaultValueSql: "'NY'"
defaultValueSql: "'NY'"
I got an error when the value provided was "NY"then i realized that they are expecting a SQL value like "GETDATE()"so i tried "'NY'"and that did the trick
我在提供的值时出错,"NY"然后我意识到他们期待一个 SQL 值,就像"GETDATE()"我尝试过的那样"'NY'",并且成功了
the entire line looks like this
整条线看起来像这样
AddColumn("TABLE_NAME", "State", c => c.String(maxLength: 2, nullable: false, defaultValueSql: "'NY'"));
AddColumn("TABLE_NAME", "State", c => c.String(maxLength: 2, nullable: false, defaultValueSql: "'NY'"));
Thanks to this answer, got me on the right track
感谢这个答案,让我走上了正确的轨道
回答by Velyo
I found that just using Auto-Property Initializer on entity property is enough to get the job done.
我发现仅在实体属性上使用 Auto-Property Initializer 就足以完成工作。
For example:
例如:
public class Thing {
public bool IsBigThing { get; set; } = false;
}
回答by Liviu Sosu
For some reason, that I was unable to explain myself the approved answer does no longer works for me.
出于某种原因,我无法解释自己,批准的答案不再适合我。
It worked on another app, on the one that I am working it doesn't.
它适用于另一个应用程序,而我正在使用的应用程序却没有。
So, an alternative, but quite inefficient, solution would be to override the SaveChanges() Method as shown bellow. This method should be on the Context class.
因此,另一种但效率很低的解决方案是覆盖 SaveChanges() 方法,如下所示。这个方法应该在 Context 类上。
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries().Where(entry => entry.Entity.GetType().GetProperty("ColumnName") != null))
{
if (entry.State == EntityState.Added)
{
entry.Property("ColumnName").CurrentValue = "DefaultValue";
}
}
回答by Chris Schaller
Many of the other responses focus on how to manually intervene when these issues occur.
许多其他响应集中在发生这些问题时如何手动干预。
After generating the Migration perform either of the following changes to the migration:
Modify the Column definition to include a defaultValue or defaultSql statement:
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));Inject a SQL statement to pre-fill the existing columns, before the AlterColumn:
Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");
生成迁移后,对迁移执行以下任一更改:
修改 Column 定义以包含 defaultValue 或 defaultSql 语句:
AlterColumn("dbo.Movies", "Director", c => c.String(nullable: false, default: ""));在 AlterColumn 之前注入 SQL 语句以预填充现有列:
Sql("UPDATE dbo.Movies SET Director = '' WHERE Director IS NULL");
Keep in mind that manual changes applied to a migration script will be overwritten if you re-scaffold the migration. For the first solution, it is pretty easy to extend EF to define a default value on a field automatically as part of the migration generation.
请记住,如果您重新构建迁移,则应用于迁移脚本的手动更改将被覆盖。对于第一个解决方案,很容易扩展 EF 以自动在字段上定义默认值,作为迁移生成的一部分。
NOTE: EF doesn't automatically do this for you because the default value implementation would be different for each RDBMS provider, but also because default values have less meaning in a pure EF runtime because each row insert will provide the current value for each property, even if it is null, so the default value constraint never gets evaluated.
This AlterColumn statement is the only time that the default constraint comes into play, I guess this become a lower priority for the team that designed the SQL Server Migration Implementation.
注意:EF 不会自动为您执行此操作,因为每个 RDBMS 提供程序的默认值实现会有所不同,而且还因为默认值在纯 EF 运行时中的意义较小,因为每行插入将为每个属性提供当前值,即使它为空,所以默认值约束永远不会被评估。
这个 AlterColumn 语句是默认约束发挥作用的唯一一次,我猜这成为设计 SQL Server 迁移实现的团队的较低优先级。
The following solution combines attribute notation, model configuration conventions, and column annotations to pass through metadata to a custom Migration code generator. Steps 1 and 2 can be replaced with fluent notation for each affected field if you are not using attribute notation.
There are a lot of techniques in play here, feel free to use some or all, I hope that there is value for everyone here
以下解决方案结合了属性表示法、模型配置约定和列注释,以将元数据传递给自定义迁移代码生成器。如果您不使用属性表示法,步骤 1 和 2 可以替换为每个受影响字段的流利表示法。
这里有很多技巧在玩,随意使用一些或全部,希望这里对每个人都有价值
Declare the Default Value
Create or re-purpose an existing attribute to define the default value to use, for this example we will create a new attribute called DefaultValue that inherits from ComponentModel.DefaultValueAttribute, as the usage is intuitive and there is a chance that existing code bases already implement this attribute. With this implementation you only need to use this specific attribute to access DefaultValueSql which is useful for dates and other custom scenarios.Implementation
[DefaultValue("Insert DefaultValue Here")] [Required] /// <--- NEW public string Director { get; set; } // Example of default value sql [DefaultValue(DefaultValueSql: "GetDate()")] [Required] public string LastModified { get; set; }Attrribute Definition
namespace EFExtensions { /// <summary> /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/> /// </summary> public class DefaultValueAttribute : System.ComponentModel.DefaultValueAttribute { /// <summary> /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/> /// </summary> public DefaultValueAttribute() : base("") { } /// <i /// <summary> /// Optional SQL to use to specify the default value. /// </summary> public string DefaultSql { get; set; } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a Unicode character. /// </summary> /// <param name="value"> /// A Unicode character that is the default value. /// </param> public DefaultValueAttribute(char value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using an 8-bit unsigned integer. /// </summary> /// <param name="value"> /// An 8-bit unsigned integer that is the default value. /// </param> public DefaultValueAttribute(byte value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 16-bit signed integer. /// </summary> /// <param name="value"> /// A 16-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(short value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 32-bit signed integer. /// </summary> /// <param name="value"> /// A 32-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(int value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 64-bit signed integer. /// </summary> /// <param name="value"> /// A 64-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(long value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a single-precision floating point number. /// </summary> /// <param name="value"> /// A single-precision floating point number that is the default value. /// </param> public DefaultValueAttribute(float value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a double-precision floating point number. /// </summary> /// <param name="value"> /// A double-precision floating point number that is the default value. /// </param> public DefaultValueAttribute(double value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a System.Boolean value. /// </summary> /// <param name="value"> /// A System.Boolean that is the default value. /// </param> public DefaultValueAttribute(bool value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a System.String. /// </summary> /// <param name="value"> /// A System.String that is the default value. /// </param> public DefaultValueAttribute(string value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class. /// </summary> /// <param name="value"> /// An System.Object that represents the default value. /// </param> public DefaultValueAttribute(object value) : base(value) { } /// /// <inheritdoc/> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class, converting the specified value to the specified type, and using an invariant /// culture as the translation context. /// </summary> /// <param name="type"> /// A System.Type that represents the type to convert the value to. /// </param> /// <param name="value"> /// A System.String that can be converted to the type using the System.ComponentModel.TypeConverter /// for the type and the U.S. English culture. /// </param> public DefaultValueAttribute(Type type, string value) : base(value) { } } }Create a convention to inject the default value into the column annotations
Column annotations are used to used to pass custom metadata about columns through to the migration script generator.
Using a convention to do this demonstrates the power behind Attribute notation to simplify how fluent metadata can be defined and manipulated for many properties rather than specifying it individually for each field.namespace EFExtensions { /// <summary> /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute /// </summary> public class DefaultValueConvention : Convention { /// <summary> /// Annotation Key to use for Default Values specified directly as an object /// </summary> public const string DirectValueAnnotationKey = "DefaultValue"; /// <summary> /// Annotation Key to use for Default Values specified as SQL Strings /// </summary> public const string SqlValueAnnotationKey = "DefaultSql"; /// <summary> /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute /// </summary> public DefaultValueConvention() { // Implement SO Default Value Attributes first this.Properties() .Where(x => x.HasAttribute<EFExtensions.DefaultValueAttribute>()) .Configure(c => c.HasColumnAnnotation( c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeKey(), c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeValue() )); // Implement Component Model Default Value Attributes, but only if it is not the SO implementation this.Properties() .Where(x => x.HasAttribute<System.ComponentModel.DefaultValueAttribute>()) .Where(x => !x.HasAttribute<MiniTuber.DataAnnotations.DefaultValueAttribute>()) .Configure(c => c.HasColumnAnnotation( DefaultValueConvention.DirectValueAnnotationKey, c.GetAttribute<System.ComponentModel.DefaultValueAttribute>().Value )); } } /// <summary> /// Extension Methods to simplify the logic for building column annotations for Default Value processing /// </summary> public static partial class PropertyInfoAttributeExtensions { /// <summary> /// Wrapper to simplify the lookup for a specific attribute on a property info. /// </summary> /// <typeparam name="T">Type of attribute to lookup</typeparam> /// <param name="self">PropertyInfo to inspect</param> /// <returns>True if an attribute of the requested type exists</returns> public static bool HasAttribute<T>(this PropertyInfo self) where T : Attribute { return self.GetCustomAttributes(false).OfType<T>().Any(); } /// <summary> /// Wrapper to return the first attribute of the specified type /// </summary> /// <typeparam name="T">Type of attribute to return</typeparam> /// <param name="self">PropertyInfo to inspect</param> /// <returns>First attribuite that matches the requested type</returns> public static T GetAttribute<T>(this System.Data.Entity.ModelConfiguration.Configuration.ConventionPrimitivePropertyConfiguration self) where T : Attribute { return self.ClrPropertyInfo.GetCustomAttributes(false).OfType<T>().First(); } /// <summary> /// Helper to select the correct DefaultValue annotation key based on the attribute values /// </summary> /// <param name="self"></param> /// <returns></returns> public static string GetDefaultValueAttributeKey(this EFExtensions.DefaultValueAttribute self) { return String.IsNullOrWhiteSpace(self.DefaultSql) ? DefaultValueConvention.DirectValueAnnotationKey : DefaultValueConvention.SqlValueAnnotationKey; } /// <summary> /// Helper to select the correct attribute property to send as a DefaultValue annotation value /// </summary> /// <param name="self"></param> /// <returns></returns> public static object GetDefaultValueAttributeValue(this EFExtensions.DefaultValueAttribute self) { return String.IsNullOrWhiteSpace(self.DefaultSql) ? self.Value : self.DefaultSql; } } }Add the Convention to the DbContext
There are many ways to acheive this, I like to declare the conventions as the first custom step in my ModelCreation logic, this will be in your DbContext class.protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Use our new DefaultValueConvention modelBuilder.Conventions.Add<EFExtensions.DefaultValueConvention>(); // My personal favourites ;) modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); }Override the MigrationCodeGenerator
Now that those annotations have been applied to the column definitions within the model, we need to modify the migration script generator to use those annotations. For this we will inherit from theSystem.Data.Entity.Migrations.Design.CSharpMigrationCodeGeneratoras we only need to inject a minimal amount of change.
Once we have processed our custom annotation, we need to remove it from the column definition to prevent it being serialised to the final output.See the base class code to explore other usage: http://entityframework.codeplex.com/sourcecontrol/latest#src/EntityFramework/Migrations/Design/CSharpMigrationCodeGenerator.cs
namespace EFExtensions { /// <summary> /// Implement DefaultValue constraint definition in Migration Scripts. /// </summary> /// <remarks> /// Original guide that provided inspiration for this https://romiller.com/2012/11/30/code-first-migrations-customizing-scaffolded-code/ /// </remarks> public class CustomCodeGenerator : System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator { /// <summary> /// Inject Default values from the DefaultValue attribute, if the DefaultValueConvention has been enabled. /// </summary> /// <seealso cref="DefaultValueConvention"/> /// <param name="column"></param> /// <param name="writer"></param> /// <param name="emitName"></param> protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false) { var annotations = column.Annotations?.ToList(); if (annotations != null && annotations.Any()) { for (int index = 0; index < annotations.Count; index ++) { var annotation = annotations[index]; bool handled = true; try { switch (annotation.Key) { case DefaultValueConvention.SqlValueAnnotationKey: if (annotation.Value?.NewValue != null) { column.DefaultValueSql = $"{annotation.Value.NewValue}"; } break; case DefaultValueConvention.DirectValueAnnotationKey: if (annotation.Value?.NewValue != null) { column.DefaultValue = Convert.ChangeType(annotation.Value.NewValue, column.ClrType); } break; default: handled = false; break; } } catch(Exception ex) { // re-throw with specific debug information throw new ApplicationException($"Failed to Implement Column Annotation for column: {column.Name} with key: {annotation.Key} and new value: {annotation.Value.NewValue}", ex); } if(handled) { // remove the annotation, it has been applied column.Annotations.Remove(annotation.Key); } } } base.Generate(column, writer, emitName); } /// <summary> /// Generates class summary comments and default attributes /// </summary> /// <param name="writer"> Text writer to add the generated code to. </param> /// <param name="designer"> A value indicating if this class is being generated for a code-behind file. </param> protected override void WriteClassAttributes(IndentedTextWriter writer, bool designer) { writer.WriteLine("/// <summary>"); writer.WriteLine("/// Definition of the Migration: {0}", this.ClassName); writer.WriteLine("/// </summary>"); writer.WriteLine("/// <remarks>"); writer.WriteLine("/// Generated Time: {0}", DateTime.Now); writer.WriteLine("/// Generated By: {0}", Environment.UserName); writer.WriteLine("/// </remarks>"); base.WriteClassAttributes(writer, designer); } } }Register the CustomCodeGenerator
Last step, in the DbMigration Configuration file we need to specify the Code Generator to use, look for Configuration.cs in your Migration folder by default...internal sealed class Configuration : DbMigrationsConfiguration<YourApplication.Database.Context> { public Configuration() { // I recommend that auto-migrations be disabled so that we control // the migrations explicitly AutomaticMigrationsEnabled = false; CodeGenerator = new EFExtensions.CustomCodeGenerator(); } protected override void Seed(YourApplication.Database.Context context) { // Your custom seed logic here } }
声明默认值
创建或重新利用现有属性来定义要使用的默认值,在本示例中,我们将创建一个名为 DefaultValue 的新属性,该属性继承自 ComponentModel.DefaultValueAttribute,因为用法很直观,并且有可能现有的代码库已经实现了这个属性。使用此实现,您只需要使用此特定属性来访问 DefaultValueSql,这对日期和其他自定义场景很有用。执行
[DefaultValue("Insert DefaultValue Here")] [Required] /// <--- NEW public string Director { get; set; } // Example of default value sql [DefaultValue(DefaultValueSql: "GetDate()")] [Required] public string LastModified { get; set; }属性定义
namespace EFExtensions { /// <summary> /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/> /// </summary> public class DefaultValueAttribute : System.ComponentModel.DefaultValueAttribute { /// <summary> /// Specifies the default value for a property but allows a custom SQL statement to be provided as well. <see cref="MiniTuber.Database.Conventions.DefaultValueConvention"/> /// </summary> public DefaultValueAttribute() : base("") { } /// <i /// <summary> /// Optional SQL to use to specify the default value. /// </summary> public string DefaultSql { get; set; } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a Unicode character. /// </summary> /// <param name="value"> /// A Unicode character that is the default value. /// </param> public DefaultValueAttribute(char value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using an 8-bit unsigned integer. /// </summary> /// <param name="value"> /// An 8-bit unsigned integer that is the default value. /// </param> public DefaultValueAttribute(byte value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 16-bit signed integer. /// </summary> /// <param name="value"> /// A 16-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(short value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 32-bit signed integer. /// </summary> /// <param name="value"> /// A 32-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(int value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a 64-bit signed integer. /// </summary> /// <param name="value"> /// A 64-bit signed integer that is the default value. /// </param> public DefaultValueAttribute(long value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a single-precision floating point number. /// </summary> /// <param name="value"> /// A single-precision floating point number that is the default value. /// </param> public DefaultValueAttribute(float value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a double-precision floating point number. /// </summary> /// <param name="value"> /// A double-precision floating point number that is the default value. /// </param> public DefaultValueAttribute(double value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a System.Boolean value. /// </summary> /// <param name="value"> /// A System.Boolean that is the default value. /// </param> public DefaultValueAttribute(bool value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class using a System.String. /// </summary> /// <param name="value"> /// A System.String that is the default value. /// </param> public DefaultValueAttribute(string value) : base(value) { } /// <summary> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class. /// </summary> /// <param name="value"> /// An System.Object that represents the default value. /// </param> public DefaultValueAttribute(object value) : base(value) { } /// /// <inheritdoc/> /// Initializes a new instance of the System.ComponentModel.DefaultValueAttribute /// class, converting the specified value to the specified type, and using an invariant /// culture as the translation context. /// </summary> /// <param name="type"> /// A System.Type that represents the type to convert the value to. /// </param> /// <param name="value"> /// A System.String that can be converted to the type using the System.ComponentModel.TypeConverter /// for the type and the U.S. English culture. /// </param> public DefaultValueAttribute(Type type, string value) : base(value) { } } }创建约定以将默认值注入列注释
列注释用于将有关列的自定义元数据传递给迁移脚本生成器。
使用约定来执行此操作展示了 Attribute 符号背后的强大功能,可以简化如何为许多属性定义和操作流畅的元数据,而不是为每个字段单独指定它。namespace EFExtensions { /// <summary> /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute /// </summary> public class DefaultValueConvention : Convention { /// <summary> /// Annotation Key to use for Default Values specified directly as an object /// </summary> public const string DirectValueAnnotationKey = "DefaultValue"; /// <summary> /// Annotation Key to use for Default Values specified as SQL Strings /// </summary> public const string SqlValueAnnotationKey = "DefaultSql"; /// <summary> /// Implement SQL Default Values from System.ComponentModel.DefaultValueAttribute /// </summary> public DefaultValueConvention() { // Implement SO Default Value Attributes first this.Properties() .Where(x => x.HasAttribute<EFExtensions.DefaultValueAttribute>()) .Configure(c => c.HasColumnAnnotation( c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeKey(), c.GetAttribute<EFExtensions.DefaultValueAttribute>().GetDefaultValueAttributeValue() )); // Implement Component Model Default Value Attributes, but only if it is not the SO implementation this.Properties() .Where(x => x.HasAttribute<System.ComponentModel.DefaultValueAttribute>()) .Where(x => !x.HasAttribute<MiniTuber.DataAnnotations.DefaultValueAttribute>()) .Configure(c => c.HasColumnAnnotation( DefaultValueConvention.DirectValueAnnotationKey, c.GetAttribute<System.ComponentModel.DefaultValueAttribute>().Value )); } } /// <summary> /// Extension Methods to simplify the logic for building column annotations for Default Value processing /// </summary> public static partial class PropertyInfoAttributeExtensions { /// <summary> /// Wrapper to simplify the lookup for a specific attribute on a property info. /// </summary> /// <typeparam name="T">Type of attribute to lookup</typeparam> /// <param name="self">PropertyInfo to inspect</param> /// <returns>True if an attribute of the requested type exists</returns> public static bool HasAttribute<T>(this PropertyInfo self) where T : Attribute { return self.GetCustomAttributes(false).OfType<T>().Any(); } /// <summary> /// Wrapper to return the first attribute of the specified type /// </summary> /// <typeparam name="T">Type of attribute to return</typeparam> /// <param name="self">PropertyInfo to inspect</param> /// <returns>First attribuite that matches the requested type</returns> public static T GetAttribute<T>(this System.Data.Entity.ModelConfiguration.Configuration.ConventionPrimitivePropertyConfiguration self) where T : Attribute { return self.ClrPropertyInfo.GetCustomAttributes(false).OfType<T>().First(); } /// <summary> /// Helper to select the correct DefaultValue annotation key based on the attribute values /// </summary> /// <param name="self"></param> /// <returns></returns> public static string GetDefaultValueAttributeKey(this EFExtensions.DefaultValueAttribute self) { return String.IsNullOrWhiteSpace(self.DefaultSql) ? DefaultValueConvention.DirectValueAnnotationKey : DefaultValueConvention.SqlValueAnnotationKey; } /// <summary> /// Helper to select the correct attribute property to send as a DefaultValue annotation value /// </summary> /// <param name="self"></param> /// <returns></returns> public static object GetDefaultValueAttributeValue(this EFExtensions.DefaultValueAttribute self) { return String.IsNullOrWhiteSpace(self.DefaultSql) ? self.Value : self.DefaultSql; } } }将约定添加到 DbContext
有很多方法可以实现这一点,我喜欢将约定声明为我的 ModelCreation 逻辑中的第一个自定义步骤,这将在您的 DbContext 类中。protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Use our new DefaultValueConvention modelBuilder.Conventions.Add<EFExtensions.DefaultValueConvention>(); // My personal favourites ;) modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); }覆盖 MigrationCodeGenerator
既然这些注释已应用于模型中的列定义,我们需要修改迁移脚本生成器以使用这些注释。为此,我们将继承自 ,System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator因为我们只需要注入最少量的更改。
处理完自定义注释后,我们需要将其从列定义中删除,以防止将其序列化为最终输出。查看基类代码以探索其他用法:http: //entityframework.codeplex.com/sourcecontrol/latest#src/EntityFramework/Migrations/Design/CSharpMigrationCodeGenerator.cs
namespace EFExtensions { /// <summary> /// Implement DefaultValue constraint definition in Migration Scripts. /// </summary> /// <remarks> /// Original guide that provided inspiration for this https://romiller.com/2012/11/30/code-first-migrations-customizing-scaffolded-code/ /// </remarks> public class CustomCodeGenerator : System.Data.Entity.Migrations.Design.CSharpMigrationCodeGenerator { /// <summary> /// Inject Default values from the DefaultValue attribute, if the DefaultValueConvention has been enabled. /// </summary> /// <seealso cref="DefaultValueConvention"/> /// <param name="column"></param> /// <param name="writer"></param> /// <param name="emitName"></param> protected override void Generate(ColumnModel column, IndentedTextWriter writer, bool emitName = false) { var annotations = column.Annotations?.ToList(); if (annotations != null && annotations.Any()) { for (int index = 0; index < annotations.Count; index ++) { var annotation = annotations[index]; bool handled = true; try { switch (annotation.Key) { case DefaultValueConvention.SqlValueAnnotationKey: if (annotation.Value?.NewValue != null) { column.DefaultValueSql = $"{annotation.Value.NewValue}"; } break; case DefaultValueConvention.DirectValueAnnotationKey: if (annotation.Value?.NewValue != null) { column.DefaultValue = Convert.ChangeType(annotation.Value.NewValue, column.ClrType); } break; default: handled = false; break; } } catch(Exception ex) { // re-throw with specific debug information throw new ApplicationException($"Failed to Implement Column Annotation for column: {column.Name} with key: {annotation.Key} and new value: {annotation.Value.NewValue}", ex); } if(handled) { // remove the annotation, it has been applied column.Annotations.Remove(annotation.Key); } } } base.Generate(column, writer, emitName); } /// <summary> /// Generates class summary comments and default attributes /// </summary> /// <param name="writer"> Text writer to add the generated code to. </param> /// <param name="designer"> A value indicating if this class is being generated for a code-behind file. </param> protected override void WriteClassAttributes(IndentedTextWriter writer, bool designer) { writer.WriteLine("/// <summary>"); writer.WriteLine("/// Definition of the Migration: {0}", this.ClassName); writer.WriteLine("/// </summary>"); writer.WriteLine("/// <remarks>"); writer.WriteLine("/// Generated Time: {0}", DateTime.Now); writer.WriteLine("/// Generated By: {0}", Environment.UserName); writer.WriteLine("/// </remarks>"); base.WriteClassAttributes(writer, designer); } } }注册 CustomCodeGenerator
最后一步,在 DbMigration 配置文件中,我们需要指定要使用的代码生成器,默认在您的 Migration 文件夹中查找 Configuration.cs...internal sealed class Configuration : DbMigrationsConfiguration<YourApplication.Database.Context> { public Configuration() { // I recommend that auto-migrations be disabled so that we control // the migrations explicitly AutomaticMigrationsEnabled = false; CodeGenerator = new EFExtensions.CustomCodeGenerator(); } protected override void Seed(YourApplication.Database.Context context) { // Your custom seed logic here } }
回答by Antoine Robin
Since EF Core 2.1, you can use MigrationBuilder.UpdateDatato change values before altering the column (cleaner than using raw SQL):
从 EF Core 2.1 开始,您可以MigrationBuilder.UpdateData在更改列之前使用更改值(比使用原始 SQL 更干净):
protected override void Up(MigrationBuilder migrationBuilder)
{
// Change existing NULL values to NOT NULL values
migrationBuilder.UpdateData(
table: tableName,
column: columnName,
value: valueInsteadOfNull,
keyColumn: columnName,
keyValue: null);
// Change column type to NOT NULL
migrationBuilder.AlterColumn<ColumnType>(
table: tableName,
name: columnName,
nullable: false,
oldClrType: typeof(ColumnType),
oldNullable: true);
}

