< Summary

Information
Class: ProjectTemplate.Infrastructure.Data.ApplicationSaveChangesPipeline
Assembly: ProjectTemplate.Infrastructure
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/ApplicationSaveChangesPipeline.cs
Line coverage
97%
Covered lines: 248
Uncovered lines: 7
Coverable lines: 255
Total lines: 515
Line coverage: 97.2%
Branch coverage
91%
Covered branches: 126
Total branches: 138
Branch coverage: 91.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/ApplicationSaveChangesPipeline.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.EntityFrameworkCore.ChangeTracking;
 3using Microsoft.Extensions.Options;
 4using ProjectTemplate.Infrastructure.Data.Auditing;
 5using ProjectTemplate.Infrastructure.Data.Entities;
 6using ProjectTemplate.Infrastructure.Data.Options;
 7
 8namespace ProjectTemplate.Infrastructure.Data;
 9
 10/// <summary>
 11/// Applies application-owned mutation, normalization, concurrency, and audit preparation during EF Core saves.
 12/// </summary>
 13public sealed class ApplicationSaveChangesPipeline :
 14    IApplicationSaveChangesPipeline,
 15    IApplicationMutationAuditReceiptAccessor
 16{
 17    private readonly ICurrentActorAccessor _currentActorAccessor;
 18    private readonly IApplicationAuditContextAccessor? _auditContextAccessor;
 19    private readonly IApplicationAuditValuePolicy _auditValuePolicy;
 20    private readonly IApplicationMutationManifestBuilder _manifestBuilder;
 21    private readonly IApplicationMutationManifestHasher _manifestHasher;
 22    private readonly bool _auditOptions;
 23    private readonly IApplicationAuditStore _auditStore;
 10024    private List<AuditEntry> _pendingAuditEntries = [];
 10025    private readonly List<AuditRecord> _activeAuditRecords = [];
 26    private ApplicationAuditContext? _activeAuditContext;
 27    private string? _activeMutationBatchId;
 28    private int _activeAuditRecordCount;
 29
 30    /// <summary>
 31    /// Initializes a new instance of the <see cref="ApplicationSaveChangesPipeline" /> class.
 32    /// </summary>
 10033    public ApplicationSaveChangesPipeline(
 10034        ICurrentActorAccessor currentActorAccessor,
 10035        IOptions<DataAccessOptions> dataAccessOptions,
 10036        IApplicationAuditStore? auditStore = null,
 10037        IApplicationAuditContextAccessor? auditContextAccessor = null,
 10038        IApplicationAuditValuePolicy? auditValuePolicy = null,
 10039        IApplicationMutationManifestBuilder? manifestBuilder = null,
 10040        IApplicationMutationManifestHasher? manifestHasher = null)
 41    {
 10042        ArgumentNullException.ThrowIfNull(currentActorAccessor);
 10043        ArgumentNullException.ThrowIfNull(dataAccessOptions);
 44
 10045        _currentActorAccessor = currentActorAccessor;
 10046        _auditContextAccessor = auditContextAccessor;
 10047        _auditValuePolicy = auditValuePolicy ?? new DefaultApplicationAuditValuePolicy();
 10048        _manifestBuilder = manifestBuilder ?? new CanonicalApplicationMutationManifestBuilder();
 10049        _manifestHasher = manifestHasher ?? new Sha256ApplicationMutationManifestHasher();
 10050        _auditOptions = dataAccessOptions.Value.Auditing.Enabled;
 10051        _auditStore = ResolveAuditStore(dataAccessOptions.Value.Auditing, auditStore);
 10052    }
 53
 54    /// <inheritdoc />
 55    public ApplicationMutationAuditReceipt? LastCompletedReceipt { get; private set; }
 56
 57    /// <inheritdoc />
 58    public bool ApplyBeforeSaveChanges(ApplicationDbContext dbContext)
 59    {
 860        ArgumentNullException.ThrowIfNull(dbContext);
 861        ResetActiveAuditState();
 62
 863        IReadOnlyList<EntityEntry> entries = GetSavePipelineEntries(dbContext);
 864        if (entries.Count == 0)
 65        {
 166            return false;
 67        }
 68
 769        ApplyPersistedStringCanonicalization(entries);
 770        ApplyLookupStringNormalization(entries);
 771        ApplyTimestampNormalization(entries);
 772        ApplyConcurrencyStamps(entries);
 73
 774        if (_auditOptions)
 75        {
 276            _pendingAuditEntries = OnBeforeSaveChanges(dbContext, entries);
 77        }
 78
 779        return true;
 80    }
 81
 82    /// <inheritdoc />
 83    public async ValueTask<bool> ApplyBeforeSaveChangesAsync(
 84        ApplicationDbContext dbContext,
 85        CancellationToken cancellationToken = default)
 86    {
 9087        ArgumentNullException.ThrowIfNull(dbContext);
 9088        cancellationToken.ThrowIfCancellationRequested();
 9089        ResetActiveAuditState();
 90
 9091        IReadOnlyList<EntityEntry> entries = GetSavePipelineEntries(dbContext);
 9092        if (entries.Count == 0)
 93        {
 294            return false;
 95        }
 96
 8897        ApplyPersistedStringCanonicalization(entries);
 8898        ApplyLookupStringNormalization(entries);
 8899        ApplyTimestampNormalization(entries);
 88100        ApplyConcurrencyStamps(entries);
 101
 88102        if (_auditOptions)
 103        {
 27104            _pendingAuditEntries = await OnBeforeSaveChangesAsync(
 27105                    dbContext,
 27106                    entries,
 27107                    cancellationToken)
 27108                .ConfigureAwait(false);
 109        }
 110
 88111        return true;
 90112    }
 113
 114    /// <inheritdoc />
 115    public bool ApplyAfterSaveChanges(ApplicationDbContext dbContext)
 116    {
 7117        ArgumentNullException.ThrowIfNull(dbContext);
 7118        bool appendedAdditionalAuditRecords = false;
 119
 14120        foreach (AuditEntry auditEntry in _pendingAuditEntries)
 121        {
 0122            CompleteTemporaryAuditProperties(auditEntry);
 0123            AppendAuditRecord(dbContext, auditEntry.ToAuditRecord());
 0124            appendedAdditionalAuditRecords = true;
 125        }
 126
 7127        CompleteMutationReceipt();
 7128        _pendingAuditEntries = [];
 7129        return appendedAdditionalAuditRecords;
 130    }
 131
 132    /// <inheritdoc />
 133    public async ValueTask<bool> ApplyAfterSaveChangesAsync(
 134        ApplicationDbContext dbContext,
 135        CancellationToken cancellationToken = default)
 136    {
 88137        ArgumentNullException.ThrowIfNull(dbContext);
 88138        cancellationToken.ThrowIfCancellationRequested();
 88139        bool appendedAdditionalAuditRecords = false;
 140
 179141        foreach (AuditEntry auditEntry in _pendingAuditEntries)
 142        {
 2143            CompleteTemporaryAuditProperties(auditEntry);
 2144            await AppendAuditRecordAsync(dbContext, auditEntry.ToAuditRecord(), cancellationToken)
 2145                .ConfigureAwait(false);
 1146            appendedAdditionalAuditRecords = true;
 147        }
 148
 87149        CompleteMutationReceipt();
 87150        _pendingAuditEntries = [];
 87151        return appendedAdditionalAuditRecords;
 87152    }
 153
 154    private void ResetActiveAuditState()
 155    {
 98156        _pendingAuditEntries = [];
 98157        _activeAuditRecords.Clear();
 98158        _activeAuditContext = null;
 98159        _activeMutationBatchId = null;
 98160        _activeAuditRecordCount = 0;
 98161    }
 162
 163    private static IReadOnlyList<EntityEntry> GetSavePipelineEntries(ApplicationDbContext dbContext)
 164    {
 98165        dbContext.ChangeTracker.DetectChanges();
 98166        return [.. dbContext.ChangeTracker
 98167            .Entries()
 98168            .Where(entry => entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)];
 169    }
 170
 171    private static void ApplyPersistedStringCanonicalization(IEnumerable<EntityEntry> entries)
 172    {
 408173        foreach (EntityEntry entry in entries)
 174        {
 109175            if (entry.Entity is AuditRecord ||
 109176                entry.State is not (EntityState.Added or EntityState.Modified))
 177            {
 178                continue;
 179            }
 180
 2812181            foreach (PropertyEntry property in entry.Properties)
 182            {
 1324183                if (property.Metadata.ClrType != typeof(string) ||
 1324184                    property.Metadata.IsPrimaryKey() ||
 1324185                    property.Metadata.IsConcurrencyToken ||
 1324186                    (entry.State == EntityState.Modified && !property.IsModified) ||
 1324187                    property.CurrentValue is not string currentValue)
 188                {
 189                    continue;
 190                }
 191
 460192                string canonicalValue = PersistenceStringCanonicalizer.Canonicalize(currentValue);
 460193                if (!string.Equals(canonicalValue, currentValue, StringComparison.Ordinal))
 194                {
 12195                    property.CurrentValue = canonicalValue;
 196                }
 197            }
 198        }
 95199    }
 200
 201    private static void ApplyLookupStringNormalization(IEnumerable<EntityEntry> entries)
 202    {
 408203        foreach (EntityEntry entry in entries)
 204        {
 109205            if (entry.Entity is not ExternalLoginAccount account ||
 109206                entry.State is not (EntityState.Added or EntityState.Modified))
 207            {
 208                continue;
 209            }
 210
 56211            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.ProviderName),
 56212                PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderName));
 56213            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.NormalizedProviderName),
 56214                PersistenceStringComparisonNormalizer.NormalizeRequiredLookupValue(account.ProviderName));
 56215            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.ProviderUserId),
 56216                PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderUserId));
 56217            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.DisplayName),
 56218                PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.DisplayName));
 56219            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.Email),
 56220                PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.Email));
 56221            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.NormalizedEmail),
 56222                PersistenceStringComparisonNormalizer.NormalizeOptionalLookupValue(account.Email));
 223        }
 95224    }
 225
 226    private static void ApplyTimestampNormalization(IEnumerable<EntityEntry> entries)
 227    {
 408228        foreach (EntityEntry entry in entries)
 229        {
 109230            if (entry.State is not (EntityState.Added or EntityState.Modified))
 231            {
 232                continue;
 233            }
 234
 4008235            foreach (PropertyEntry property in entry.Properties)
 236            {
 1896237                if (!IsUtcTimestampProperty(property.Metadata.Name))
 238                {
 239                    continue;
 240                }
 241
 324242                Type propertyType = Nullable.GetUnderlyingType(property.Metadata.ClrType)
 324243                    ?? property.Metadata.ClrType;
 244
 324245                if (propertyType == typeof(DateTime) && property.CurrentValue is DateTime dateTimeValue)
 246                {
 162247                    property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeValue);
 248                }
 162249                else if (propertyType == typeof(DateTimeOffset) && property.CurrentValue is DateTimeOffset dateTimeOffse
 250                {
 0251                    property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeOffsetValue);
 252                }
 253            }
 254        }
 95255    }
 256
 257    private static void ApplyConcurrencyStamps(IEnumerable<EntityEntry> entries)
 258    {
 408259        foreach (EntityEntry entry in entries)
 260        {
 109261            if (entry.Entity is not DataEntity entity)
 262            {
 263                continue;
 264            }
 265
 109266            PropertyEntry concurrencyStampProperty = entry.Property(nameof(DataEntity.ConcurrencyStamp));
 109267            if (entry.State == EntityState.Added)
 268            {
 91269                if (string.IsNullOrWhiteSpace(entity.ConcurrencyStamp))
 270                {
 3271                    concurrencyStampProperty.CurrentValue = DataEntity.NewConcurrencyStamp();
 272                }
 273            }
 18274            else if (entry.State == EntityState.Modified)
 275            {
 17276                concurrencyStampProperty.CurrentValue = DataEntity.NewConcurrencyStamp();
 277            }
 278        }
 95279    }
 280
 281    private static bool IsUtcTimestampProperty(string propertyName)
 282    {
 1896283        return propertyName.EndsWith("Utc", StringComparison.Ordinal);
 284    }
 285
 286    private List<AuditEntry> OnBeforeSaveChanges(
 287        ApplicationDbContext dbContext,
 288        IEnumerable<EntityEntry> entries)
 289    {
 2290        List<AuditEntry> auditEntries = CreateAuditEntries(entries);
 8291        foreach (AuditEntry auditEntry in auditEntries.Where(entry => !entry.HasTemporaryProperties))
 292        {
 2293            AppendAuditRecord(dbContext, auditEntry.ToAuditRecord());
 294        }
 295
 2296        return [.. auditEntries.Where(entry => entry.HasTemporaryProperties)];
 297    }
 298
 299    private async ValueTask<List<AuditEntry>> OnBeforeSaveChangesAsync(
 300        ApplicationDbContext dbContext,
 301        IEnumerable<EntityEntry> entries,
 302        CancellationToken cancellationToken)
 303    {
 27304        List<AuditEntry> auditEntries = CreateAuditEntries(entries);
 106305        foreach (AuditEntry auditEntry in auditEntries.Where(entry => !entry.HasTemporaryProperties))
 306        {
 26307            await AppendAuditRecordAsync(dbContext, auditEntry.ToAuditRecord(), cancellationToken)
 26308                .ConfigureAwait(false);
 309        }
 310
 27311        return [.. auditEntries.Where(entry => entry.HasTemporaryProperties)];
 27312    }
 313
 314    private void AppendAuditRecord(ApplicationDbContext dbContext, AuditRecord auditRecord)
 315    {
 2316        _auditStore.Append(dbContext, auditRecord);
 2317        _activeAuditRecords.Add(auditRecord);
 2318    }
 319
 320    private async ValueTask AppendAuditRecordAsync(
 321        ApplicationDbContext dbContext,
 322        AuditRecord auditRecord,
 323        CancellationToken cancellationToken)
 324    {
 28325        await _auditStore.AppendAsync(dbContext, auditRecord, cancellationToken).ConfigureAwait(false);
 27326        _activeAuditRecords.Add(auditRecord);
 27327    }
 328
 329    private List<AuditEntry> CreateAuditEntries(IEnumerable<EntityEntry> entries)
 330    {
 29331        var auditEntries = new List<AuditEntry>();
 29332        ApplicationAuditContext auditContext = ResolveAuditContext();
 29333        string mutationBatchId = Guid.NewGuid().ToString("N");
 334
 124335        foreach (EntityEntry entry in entries)
 336        {
 33337            if (entry.Entity is AuditRecord ||
 33338                entry.State == EntityState.Detached ||
 33339                entry.State == EntityState.Unchanged)
 340            {
 341                continue;
 342            }
 343
 30344            AuditEntry auditEntry = new(entry)
 30345            {
 30346                TableName = entry.Metadata.GetTableName() ?? string.Empty,
 30347                ModifiedBy = auditContext.ActorDisplayName,
 30348                ActorId = auditContext.ActorId,
 30349                ActorType = auditContext.ActorType,
 30350                MutationBatchId = mutationBatchId,
 30351                OperationExecutionId = auditContext.OperationExecutionId,
 30352                ExecutionAttemptId = auditContext.ExecutionAttemptId,
 30353                CorrelationId = auditContext.CorrelationId,
 30354                TraceId = auditContext.TraceId,
 30355                SpanId = auditContext.SpanId,
 30356                DecisionAuditRecordId = auditContext.DecisionAuditRecordId,
 30357                TenantHash = auditContext.TenantHash,
 30358                OrganizationHash = auditContext.OrganizationHash,
 30359                ModifiedOnUtc = PersistenceTimestamp.UtcNow(),
 30360                State = entry.State.ToString()
 30361            };
 30362            auditEntries.Add(auditEntry);
 363
 784364            foreach (PropertyEntry property in entry.Properties)
 365            {
 362366                if (property.IsTemporary)
 367                {
 2368                    auditEntry.TemporaryProperties.Add(property);
 2369                    continue;
 370                }
 371
 360372                string propertyName = property.Metadata.Name;
 360373                if (property.Metadata.IsPrimaryKey())
 374                {
 28375                    AddProtectedValue(auditEntry.KeyValues, entry, propertyName, property.CurrentValue);
 28376                    continue;
 377                }
 378
 332379                switch (entry.State)
 380                {
 381                    case EntityState.Added:
 299382                        AddProtectedValue(auditEntry.CurrentValues, entry, propertyName, property.CurrentValue);
 299383                        break;
 384                    case EntityState.Deleted:
 11385                        AddProtectedValue(auditEntry.OriginalValues, entry, propertyName, property.OriginalValue);
 11386                        break;
 22387                    case EntityState.Modified when property.IsModified:
 5388                        AddProtectedValue(auditEntry.OriginalValues, entry, propertyName, property.OriginalValue);
 5389                        AddProtectedValue(auditEntry.CurrentValues, entry, propertyName, property.CurrentValue);
 5390                        break;
 391                    case EntityState.Modified:
 392                    case EntityState.Detached:
 393                    case EntityState.Unchanged:
 394                        break;
 395                    default:
 0396                        throw new InvalidOperationException($"Unsupported entity state '{entry.State}'.");
 397                }
 398            }
 399        }
 400
 29401        if (auditEntries.Count > 0)
 402        {
 26403            _activeAuditContext = auditContext;
 26404            _activeMutationBatchId = mutationBatchId;
 26405            _activeAuditRecordCount = auditEntries.Count;
 406        }
 407
 29408        return auditEntries;
 409    }
 410
 411    private ApplicationAuditContext ResolveAuditContext()
 412    {
 29413        if (_auditContextAccessor is not null)
 414        {
 3415            return _auditContextAccessor.Current
 3416                ?? throw new InvalidOperationException("The application audit context accessor returned no context.");
 417        }
 418
 26419        string displayActor = _currentActorAccessor.CurrentActor;
 26420        string actorId = string.IsNullOrWhiteSpace(displayActor) ? "Unknown" : displayActor.Trim();
 26421        return new ApplicationAuditContext(
 26422            actorId,
 26423            actorId.Equals(SystemCurrentActorAccessor.ActorName, StringComparison.Ordinal)
 26424                ? ApplicationAuditActorTypes.System
 26425                : ApplicationAuditActorTypes.Unknown,
 26426            actorId);
 427    }
 428
 429    private void AddProtectedValue(
 430        Dictionary<string, object> target,
 431        EntityEntry entry,
 432        string propertyName,
 433        object? value)
 434    {
 350435        if (ApplicationAuditValueProtector.TryProtect(
 350436            _auditValuePolicy,
 350437            entry.Entity.GetType(),
 350438            propertyName,
 350439            value,
 350440            out object protectedValue))
 441        {
 348442            target[propertyName] = protectedValue;
 443        }
 350444    }
 445
 446    private static void SetPropertyCurrentValue(EntityEntry entry, string propertyName, object? value)
 447    {
 336448        entry.Property(propertyName).CurrentValue = value;
 336449    }
 450
 451    private void CompleteTemporaryAuditProperties(AuditEntry auditEntry)
 452    {
 8453        foreach (PropertyEntry property in auditEntry.TemporaryProperties)
 454        {
 2455            if (property.Metadata.IsPrimaryKey())
 456            {
 2457                AddProtectedValue(auditEntry.KeyValues, auditEntry.Entry, property.Metadata.Name, property.CurrentValue)
 458            }
 459            else
 460            {
 0461                AddProtectedValue(auditEntry.CurrentValues, auditEntry.Entry, property.Metadata.Name, property.CurrentVa
 462            }
 463        }
 2464    }
 465
 466    private void CompleteMutationReceipt()
 467    {
 94468        if (_activeAuditContext is null ||
 94469            string.IsNullOrWhiteSpace(_activeMutationBatchId) ||
 94470            _activeAuditRecordCount == 0)
 471        {
 70472            return;
 473        }
 474
 24475        if (_activeAuditRecords.Count != _activeAuditRecordCount)
 476        {
 0477            throw new InvalidOperationException("The mutation audit receipt cannot be completed because the retained aud
 478        }
 479
 24480        ApplicationMutationManifest manifest = _manifestBuilder.Build(_activeAuditRecords);
 24481        string manifestHash = _manifestHasher.ComputeHash(manifest);
 482
 24483        LastCompletedReceipt = new ApplicationMutationAuditReceipt(
 24484            _activeMutationBatchId,
 24485            _activeAuditRecordCount,
 24486            "Committed",
 24487            DateTimeOffset.UtcNow,
 24488            manifestHash,
 24489            _manifestHasher.Algorithm,
 24490            manifest.SchemaVersion,
 24491            _activeAuditContext.OperationExecutionId,
 24492            _activeAuditContext.ExecutionAttemptId,
 24493            _activeAuditContext.DecisionAuditRecordId,
 24494            _activeAuditContext.CorrelationId,
 24495            _activeAuditContext.TraceId);
 496
 24497        _activeAuditContext = null;
 24498        _activeMutationBatchId = null;
 24499        _activeAuditRecordCount = 0;
 24500        _activeAuditRecords.Clear();
 24501    }
 502
 503    private static IApplicationAuditStore ResolveAuditStore(
 504        DataAuditingOptions auditingOptions,
 505        IApplicationAuditStore? auditStore)
 506    {
 100507        ArgumentNullException.ThrowIfNull(auditingOptions);
 100508        return auditStore is not null
 100509            ? auditStore
 100510            : !auditingOptions.Enabled || AuditStorageModes.IsLocal(auditingOptions.StorageMode)
 100511                ? new LocalApplicationAuditStore()
 100512                : throw new InvalidOperationException(
 100513                    $"Application audit storage mode '{AuditStorageModes.Normalize(auditingOptions.StorageMode)}' requir
 514    }
 515}

Methods/Properties

.ctor(ProjectTemplate.Infrastructure.Data.ICurrentActorAccessor,Microsoft.Extensions.Options.IOptions`1<ProjectTemplate.Infrastructure.Data.Options.DataAccessOptions>,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationAuditStore,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationAuditContextAccessor,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationAuditValuePolicy,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationMutationManifestBuilder,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationMutationManifestHasher)
ApplyBeforeSaveChanges(ProjectTemplate.Infrastructure.Data.ApplicationDbContext)
ApplyBeforeSaveChangesAsync()
ApplyAfterSaveChanges(ProjectTemplate.Infrastructure.Data.ApplicationDbContext)
ApplyAfterSaveChangesAsync()
ResetActiveAuditState()
GetSavePipelineEntries(ProjectTemplate.Infrastructure.Data.ApplicationDbContext)
ApplyPersistedStringCanonicalization(System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
ApplyLookupStringNormalization(System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
ApplyTimestampNormalization(System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
ApplyConcurrencyStamps(System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
IsUtcTimestampProperty(System.String)
OnBeforeSaveChanges(ProjectTemplate.Infrastructure.Data.ApplicationDbContext,System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
OnBeforeSaveChangesAsync()
AppendAuditRecord(ProjectTemplate.Infrastructure.Data.ApplicationDbContext,ProjectTemplate.Infrastructure.Data.Entities.AuditRecord)
AppendAuditRecordAsync()
CreateAuditEntries(System.Collections.Generic.IEnumerable`1<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>)
ResolveAuditContext()
AddProtectedValue(System.Collections.Generic.Dictionary`2<System.String,System.Object>,Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,System.String,System.Object)
SetPropertyCurrentValue(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry,System.String,System.Object)
CompleteTemporaryAuditProperties(ProjectTemplate.Infrastructure.Data.AuditEntry)
CompleteMutationReceipt()
ResolveAuditStore(ProjectTemplate.Infrastructure.Data.Options.DataAuditingOptions,ProjectTemplate.Infrastructure.Data.Auditing.IApplicationAuditStore)