Все мои сущности расширяются 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 установить созданные / измененные свойства перед сохранением. Для этого я добавил следующее при настройке 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");
}
}
Сохраняя изменения, вызывая SaveChanges()
на DbContext
, я получаю следующее исключение:
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()
обязательны для заполнения, в противном случае они неправильно перечислили
Помимо того, что идентификаторы содержат ожидаемое мной значение, свойство ModifiedById
не является Nullable и, следовательно, никогда не должно иметь значение null (оно может содержать 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"
});
}
[...]
}
Repository:
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();
}