c# - 如何设置阴影属性的默认值
问题描述
我有以下实体:
public class Person
{
public Guid Id { get; set; }
public string Name { get; set; }
}
这是我的数据库上下文
public class PersonDbContext : DbContext
{
private static readonly ILoggerFactory
Logger = LoggerFactory.Create(x => x.AddConsole());
public DbSet<Person> Persons { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLoggerFactory(Logger)
.UseSqlServer(
"Server=(localdb)\\mssqllocaldb;Database=PersonDb;Trusted_Connection=True;MultipleActiveResultSets=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Person>()
.Property<DateTime>("Created")
.HasDefaultValueSql("GETUTCDATE()")
.ValueGeneratedOnAdd();
modelBuilder
.Entity<Person>()
.Property<DateTime>("Updated")
.HasDefaultValueSql("GETUTCDATE()")
.ValueGeneratedOnAddOrUpdate();
}
}
从OnModelCreating
覆盖中可以看出,我正在向Person
实体添加阴影属性更新/创建。
我将这些属性设置为使用 SQL 默认值填充
Created
增值时Updated
添加或更新价值时
下面是客户端代码
var personId = Guid.Parse("CF5EE27D-C694-408A-9F7B-080FF6315843");
using (var dbContext = new PersonDbContext())
{
var person = new Person
{
Id = personId,
Name = "New Person"
};
dbContext.Add(person);
await dbContext.SaveChangesAsync();
}
using (var dbContext = new PersonDbContext())
{
var person = dbContext.Persons.Find(personId);
var personName = person.Name;
person.Name = $"{personName} {DateTime.UtcNow}";
dbContext.SaveChanges();
}
我可以确认插入新人时这两个属性都设置为 UTC 日期。但是,在更新时,Updated
未设置属性。
这是生成的 t-sql:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (1ms) [Parameters=[@p1='?' (DbType = Guid), @p0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Persons] SET [Name] = @p0
WHERE [Id] = @p1;
SELECT [Updated]
FROM [Persons]
WHERE @@ROWCOUNT = 1 AND [Id] = @p1;
在添加或更新时阅读有关生成值的文档,我看到以下警告:
但是,如果您指定在添加或更新时生成 DateTime 属性,则必须设置生成值的方式。一种方法是配置 GETDATE() 的默认值(请参阅默认值)以生成新行的值。然后,您可以使用数据库触发器在更新期间生成值(例如以下示例触发器)。
我不明白那时的目的是什么ValueGeneratedOnAddOrUpdate()
,如果它表现得像ValueGeneratedOnAdd()
我必须手动干预(创建触发器)来设置这个属性。
确实,如果我将Updated
shadow 属性的定义更改为
modelBuilder
.Entity<Person>()
.Property<DateTime>("Updated")
.HasDefaultValueSql("GETUTCDATE()")
.ValueGeneratedOnAdd();
SaveChanges
并覆盖PersonDbContext
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
foreach (var entry in ChangeTracker.Entries().Where(entity => entity.State == EntityState.Modified))
{
entry.Property("Updated").CurrentValue = DateTime.UtcNow;
}
return base.SaveChanges();
}
这符合预期。
所以问题是 - 在 EF Core 中为阴影属性设置默认值的正确方法是什么。
这是我更大项目中的简化示例,因此HasData
在OnModelCreating
覆盖中的实体上使用不是一个好的选择(由于实体很多)。
我正在使用 EF Core 3.1.1
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1"/>
解决方案
如果您想拥有可重用的阴影属性,请按照以下步骤操作。
1-创建一个标记空界面。IAuditableEntity.cs
/// <summary>
/// It's a marker interface, in order to make our entities audit-able.
/// Every entity you mark with this interface, will save audit info to the database.
/// </summary>
public interface IAuditableEntity
{ }
2- 创建一个静态类来编写你的影子属性逻辑。AuditableShadowProperties.cs
public static class AuditableShadowProperties {
public static readonly Func<object, DateTimeOffset?> EfPropertyCreatedDateTime =
entity => EF.Property<DateTimeOffset?> (entity, CreatedDateTime);
public static readonly string CreatedDateTime = nameof (CreatedDateTime);
public static readonly Func<object, DateTimeOffset?> EfPropertyModifiedDateTime =
entity => EF.Property<DateTimeOffset?> (entity, ModifiedDateTime);
public static readonly string ModifiedDateTime = nameof (ModifiedDateTime);
public static void AddAuditableShadowProperties (this ModelBuilder modelBuilder) {
foreach (var entityType in modelBuilder.Model
.GetEntityTypes ()
.Where (e => typeof (IAuditableEntity).IsAssignableFrom (e.ClrType))) {
modelBuilder.Entity (entityType.ClrType)
.Property<DateTimeOffset?> (CreatedDateTime);
modelBuilder.Entity (entityType.ClrType)
.Property<DateTimeOffset?> (ModifiedDateTime);
}
}
public static void SetAuditableEntityPropertyValues (
this ChangeTracker changeTracker) {
var now = DateTimeOffset.UtcNow;
var modifiedEntries = changeTracker.Entries<IAuditableEntity> ()
.Where (x => x.State == EntityState.Modified);
foreach (var modifiedEntry in modifiedEntries) {
modifiedEntry.Property (ModifiedDateTime).CurrentValue = now;
}
var addedEntries = changeTracker.Entries<IAuditableEntity> ()
.Where (x => x.State == EntityState.Added);
foreach (var addedEntry in addedEntries) {
addedEntry.Property (CreatedDateTime).CurrentValue = now;
}
}
}
3-向您添加必要的更改PersonDbContext
以使用您的IAuditableEntity
.
// first we add our shadow properties to the database with next migration
protected override void OnModelCreating(ModelBuilder builder)
{
...
builder.AddAuditableShadowProperties();
}
// override saveChanges methods to use our shadow properties.
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
BeforeSaveTriggers();
ChangeTracker.AutoDetectChangesEnabled =
false; // for performance reasons, to avoid calling DetectChanges() again.
var result = base.SaveChanges();
ChangeTracker.AutoDetectChangesEnabled = true;
return result;
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
ChangeTracker.DetectChanges();
BeforeSaveTriggers();
ChangeTracker.AutoDetectChangesEnabled =
false; // for performance reasons, to avoid calling DetectChanges() again.
var result = base.SaveChangesAsync(cancellationToken);
ChangeTracker.AutoDetectChangesEnabled = true;
return result;
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = new CancellationToken())
{
ChangeTracker.DetectChanges();
BeforeSaveTriggers();
ChangeTracker.AutoDetectChangesEnabled =
false; // for performance reasons, to avoid calling DetectChanges() again.
var result = base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
ChangeTracker.AutoDetectChangesEnabled = true;
return result;
}
#region "ExtraMethods"
public T GetShadowPropertyValue<T>(object entity, string propertyName) where T : IConvertible
{
var value = this.Entry(entity).Property(propertyName).CurrentValue;
return value != null ?
(T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture) :
default(T);
}
public object GetShadowPropertyValue(object entity, string propertyName)
{
return this.Entry(entity).Property(propertyName).CurrentValue;
}
private void BeforeSaveTriggers()
{
ValidateEntities();
SetShadowProperties();
}
private void ValidateEntities()
{
var errors = this.GetValidationErrors();
if (!string.IsNullOrWhiteSpace(errors))
{
// we can't use constructor injection anymore, because we are using the `AddDbContextPool<>`
var loggerFactory = this.GetService<ILoggerFactory>();
loggerFactory.CheckArgumentIsNull(nameof(loggerFactory));
var logger = loggerFactory.CreateLogger<AppDbContext>();
logger.LogError(errors);
throw new InvalidOperationException(errors);
}
}
private void SetShadowProperties()
{
ChangeTracker.SetAuditableEntityPropertyValues();
}
#endregio
用法:
4-现在您可以将IAuditableEntity
接口添加到您想要拥有这些阴影属性的任何实体,您就完成了。
public class Person : IAuditableEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
}
我正在使用IAuditableEntity
许多其他属性,例如BrowserName
userIp
... 但我在此示例中删除了这些属性以使其尽可能简单。在此示例中解释所有内容并不容易,但如果您对这种方法有任何疑问,请随时询问。