保存更改时设置默认值
Set default value when saving changes
我所有的实体都扩展了 BaseEntity
,它具有那些(相关)属性:
namespace Sppd.TeamTuner.Core.Domain.Entities
{
public abstract class BaseEntity
{
/// <summary>
/// Unique identifier identifying a single instance of an entity.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Specifies when the entity instance has been created.
/// </summary>
public DateTime CreatedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been created.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Specifies when the entity instance has been last updated.
/// </summary>
public DateTime ModifiedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been last modified.
/// </summary>
public Guid ModifiedById { get; set; }
protected BaseEntity()
{
Id = Guid.NewGuid();
}
}
}
我想让 ef 在保存前设置 created/modified 属性。为此,我在配置 DbContext
时添加了以下内容:
private void ConfigureBaseEntity<TEntity>(EntityTypeBuilder<TEntity> builder)
where TEntity : BaseEntity
{
// Constraints
builder.Property(e => e.CreatedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
builder.Property(e => e.CreatedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAddOrUpdate();
}
还有这个ValueGenerator
:
internal class CurrentUserIdValueGenerator : ValueGenerator<Guid>
{
public override bool GeneratesTemporaryValues => false;
public override Guid Next(EntityEntry entry)
{
return GetCurrentUser(entry).Id;
}
private static ITeamTunerUser GetCurrentUser(EntityEntry entry)
{
var userProvider = entry.Context.GetService<ITeamTunerUserProvider>();
if (userProvider.CurrentUser != null)
{
return userProvider.CurrentUser;
}
if (entry.Entity is ITeamTunerUser user)
{
// Special case for user creation: The user creates himself and thus doesn't exist yet. Use him as the current user.
return user;
}
throw new BusinessException("CurrentUser not defined");
}
}
通过在 DbContext
上调用 SaveChanges()
来保留更改时,出现以下异常:
Microsoft.EntityFrameworkCore.DbUpdateException
HResult=0x80131500
Message=An error occurred while updating the entries. See the inner exception for details.
Source=Microsoft.EntityFrameworkCore.Relational
StackTrace:
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(DbContext _, ValueTuple`2 parameters)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IReadOnlyList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Sppd.TeamTuner.Infrastructure.DataAccess.EF.TeamTunerContext.SaveChanges(Boolean acceptAllChangesOnSuccess) in E:\dev\Sppd.TeamTuner\Backend\Sppd.TeamTuner.DataAccess.EF\TeamTunerContext.cs:line 48
Inner Exception 1:
SqlException: Cannot insert the value NULL into column 'ModifiedById', table 'Sppd.TeamTuner-DEV.dbo.CardType'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
检查 ChangeTracker
的内容时,所有实体都设置了 ModifiedById
:
旁注:ToList()
是必需的,否则它没有正确枚举
除了 ID 包含我期望的值这一事实外,ModifiedById
属性 不可为空,因此永远不应为空(它可能包含 default(Guid)
)。
知道发生了什么吗?
[编辑] 要添加的代码:
播种者:
internal class CardTypeDbSeeder : IDbSeeder
{
private readonly IRepository<CardType> _cardTypeRepository;
public CardTypeDbSeeder(IRepository<CardType> cardTypeRepository)
{
_cardTypeRepository = cardTypeRepository;
}
public int Priority => SeederConstants.Priority.BASE_DATA;
public void Seed()
{
_cardTypeRepository.Add(new CardType
{
Id = Guid.Parse(TestingConstants.CardType.ASSASSIN_ID),
Name = "Assassin"
});
}
[...]
}
存储库:
namespace Sppd.TeamTuner.Infrastructure.DataAccess.EF.Repositories
{
internal class Repository<TEntity> : IRepository<TEntity>
where TEntity : BaseEntity
{
protected DbSet<TEntity> Set => Context.Set<TEntity>();
protected TeamTunerContext Context { get; }
protected virtual Func<IQueryable<TEntity>, IQueryable<TEntity>> Includes { get; } = null;
public Repository(TeamTunerContext context)
{
Context = context;
}
public async Task<TEntity> GetAsync(Guid entityId)
{
TEntity entity;
try
{
entity = await GetQueryWithIncludes().SingleAsync(e => e.Id == entityId);
}
catch (InvalidOperationException)
{
throw new EntityNotFoundException(typeof(TEntity), entityId.ToString());
}
return entity;
}
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await GetQueryWithIncludes().ToListAsync();
}
public void Delete(Guid entityId)
{
var entityToDelete = GetAsync(entityId);
entityToDelete.Wait();
Set.Remove(entityToDelete.Result);
}
public void Add(TEntity entity)
{
Set.Add(entity);
}
public void Update(TEntity entity)
{
Set.Update(entity);
}
protected IQueryable<TEntity> GetQueryWithIncludes()
{
return Includes == null
? Set
: Includes(Set);
}
}
}
提交更改:
if (isNewDatabase)
{
s_logger.LogDebug($"New database created. Seed data for SeedMode={databaseConfig.SeedMode}");
foreach (var seeder in scope.ServiceProvider.GetServices<IDbSeeder>().OrderBy(seeder => seeder.Priority))
{
seeder.Seed();
s_logger.LogDebug($"Seeded {seeder.GetType().Name}");
}
// The changes are usually being saved by a unit of work. Here, while starting the application, we will do it on the context itself.
context.SaveChanges();
}
正如 GitHub 问题中的评论和大量浏览所讨论的那样,事实证明不可能为此使用价值生成器。我已经通过在 Datacontext.SaveChanges
的覆盖中实现 PrepareSaveChanges()
来解决它,它调用以下代码:
private void SetModifierMetadataProperties(EntityEntry<BaseEntity> entry, DateTime saveDate)
{
var entity = entry.Entity;
var currentUserId = GetCurrentUser(entry).Id;
if (entity.IsDeleted)
{
entity.DeletedById = currentUserId;
entity.DeletedOnUtc = saveDate;
return;
}
if (entry.State == EntityState.Added)
{
entity.CreatedById = currentUserId;
entity.CreatedOnUtc = saveDate;
}
entity.ModifiedById = currentUserId;
entity.ModifiedOnUtc = saveDate;
}
对于完整的实现,请遵循 SaveChangesAsync
覆盖的执行路径
我所有的实体都扩展了 BaseEntity
,它具有那些(相关)属性:
namespace Sppd.TeamTuner.Core.Domain.Entities
{
public abstract class BaseEntity
{
/// <summary>
/// Unique identifier identifying a single instance of an entity.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Specifies when the entity instance has been created.
/// </summary>
public DateTime CreatedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been created.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Specifies when the entity instance has been last updated.
/// </summary>
public DateTime ModifiedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been last modified.
/// </summary>
public Guid ModifiedById { get; set; }
protected BaseEntity()
{
Id = Guid.NewGuid();
}
}
}
我想让 ef 在保存前设置 created/modified 属性。为此,我在配置 DbContext
时添加了以下内容:
private void ConfigureBaseEntity<TEntity>(EntityTypeBuilder<TEntity> builder)
where TEntity : BaseEntity
{
// Constraints
builder.Property(e => e.CreatedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
builder.Property(e => e.CreatedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAddOrUpdate();
}
还有这个ValueGenerator
:
internal class CurrentUserIdValueGenerator : ValueGenerator<Guid>
{
public override bool GeneratesTemporaryValues => false;
public override Guid Next(EntityEntry entry)
{
return GetCurrentUser(entry).Id;
}
private static ITeamTunerUser GetCurrentUser(EntityEntry entry)
{
var userProvider = entry.Context.GetService<ITeamTunerUserProvider>();
if (userProvider.CurrentUser != null)
{
return userProvider.CurrentUser;
}
if (entry.Entity is ITeamTunerUser user)
{
// Special case for user creation: The user creates himself and thus doesn't exist yet. Use him as the current user.
return user;
}
throw new BusinessException("CurrentUser not defined");
}
}
通过在 DbContext
上调用 SaveChanges()
来保留更改时,出现以下异常:
Microsoft.EntityFrameworkCore.DbUpdateException
HResult=0x80131500
Message=An error occurred while updating the entries. See the inner exception for details.
Source=Microsoft.EntityFrameworkCore.Relational
StackTrace:
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(DbContext _, ValueTuple`2 parameters)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IReadOnlyList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Sppd.TeamTuner.Infrastructure.DataAccess.EF.TeamTunerContext.SaveChanges(Boolean acceptAllChangesOnSuccess) in E:\dev\Sppd.TeamTuner\Backend\Sppd.TeamTuner.DataAccess.EF\TeamTunerContext.cs:line 48
Inner Exception 1:
SqlException: Cannot insert the value NULL into column 'ModifiedById', table 'Sppd.TeamTuner-DEV.dbo.CardType'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
检查 ChangeTracker
的内容时,所有实体都设置了 ModifiedById
:
ToList()
是必需的,否则它没有正确枚举
除了 ID 包含我期望的值这一事实外,ModifiedById
属性 不可为空,因此永远不应为空(它可能包含 default(Guid)
)。
知道发生了什么吗?
[编辑] 要添加的代码:
播种者:
internal class CardTypeDbSeeder : IDbSeeder
{
private readonly IRepository<CardType> _cardTypeRepository;
public CardTypeDbSeeder(IRepository<CardType> cardTypeRepository)
{
_cardTypeRepository = cardTypeRepository;
}
public int Priority => SeederConstants.Priority.BASE_DATA;
public void Seed()
{
_cardTypeRepository.Add(new CardType
{
Id = Guid.Parse(TestingConstants.CardType.ASSASSIN_ID),
Name = "Assassin"
});
}
[...]
}
存储库:
namespace Sppd.TeamTuner.Infrastructure.DataAccess.EF.Repositories
{
internal class Repository<TEntity> : IRepository<TEntity>
where TEntity : BaseEntity
{
protected DbSet<TEntity> Set => Context.Set<TEntity>();
protected TeamTunerContext Context { get; }
protected virtual Func<IQueryable<TEntity>, IQueryable<TEntity>> Includes { get; } = null;
public Repository(TeamTunerContext context)
{
Context = context;
}
public async Task<TEntity> GetAsync(Guid entityId)
{
TEntity entity;
try
{
entity = await GetQueryWithIncludes().SingleAsync(e => e.Id == entityId);
}
catch (InvalidOperationException)
{
throw new EntityNotFoundException(typeof(TEntity), entityId.ToString());
}
return entity;
}
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await GetQueryWithIncludes().ToListAsync();
}
public void Delete(Guid entityId)
{
var entityToDelete = GetAsync(entityId);
entityToDelete.Wait();
Set.Remove(entityToDelete.Result);
}
public void Add(TEntity entity)
{
Set.Add(entity);
}
public void Update(TEntity entity)
{
Set.Update(entity);
}
protected IQueryable<TEntity> GetQueryWithIncludes()
{
return Includes == null
? Set
: Includes(Set);
}
}
}
提交更改:
if (isNewDatabase)
{
s_logger.LogDebug($"New database created. Seed data for SeedMode={databaseConfig.SeedMode}");
foreach (var seeder in scope.ServiceProvider.GetServices<IDbSeeder>().OrderBy(seeder => seeder.Priority))
{
seeder.Seed();
s_logger.LogDebug($"Seeded {seeder.GetType().Name}");
}
// The changes are usually being saved by a unit of work. Here, while starting the application, we will do it on the context itself.
context.SaveChanges();
}
正如 GitHub 问题中的评论和大量浏览所讨论的那样,事实证明不可能为此使用价值生成器。我已经通过在 Datacontext.SaveChanges
的覆盖中实现 PrepareSaveChanges()
来解决它,它调用以下代码:
private void SetModifierMetadataProperties(EntityEntry<BaseEntity> entry, DateTime saveDate)
{
var entity = entry.Entity;
var currentUserId = GetCurrentUser(entry).Id;
if (entity.IsDeleted)
{
entity.DeletedById = currentUserId;
entity.DeletedOnUtc = saveDate;
return;
}
if (entry.State == EntityState.Added)
{
entity.CreatedById = currentUserId;
entity.CreatedOnUtc = saveDate;
}
entity.ModifiedById = currentUserId;
entity.ModifiedOnUtc = saveDate;
}
对于完整的实现,请遵循 SaveChangesAsync
覆盖的执行路径