Philippe
Philippe

Reputation: 2029

Set default value when saving changes

All my entities extend BaseEntity which has those (relevant) properties:

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();
        }
    }
}

I want to let ef set the created/modified properties before saving. For this, I've added following when configuring the 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();
    }

And this 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");
    }
}

When persisting the changes by calling SaveChanges() on the DbContext, I get following exception:

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.

When checking the contents of the ChangeTracker all entities have the ModifiedById set: enter image description here Side note: The ToList() are required, otherwise it didn't enumerate correctly

Beside the fact that the IDs contain the value I expect, the ModifiedById property is not Nullable and thus should never be null (it might contain default(Guid)).

Any idea what's going on?

[Edit] Code to add:

Seeder:

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);
        }
    }
}

Commit the changes:

            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();
            }

Upvotes: 1

Views: 3073

Answers (1)

Philippe
Philippe

Reputation: 2029

As discussed in the comments and a lot of browsing in GitHub issues, it turned out that it isn't possible to use value generators for this. I've solved it by implementing a PrepareSaveChanges() in an override of Datacontext.SaveChanges which calls following code:

    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;
    }

For the full implementation, follow the exectution path from the override of SaveChangesAsync

Upvotes: 1

Related Questions