< 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: 247
Uncovered lines: 7
Coverable lines: 254
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;
 15424    private List<AuditEntry> _pendingAuditEntries = [];
 15425    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>
 15433    public ApplicationSaveChangesPipeline(
 15434        ICurrentActorAccessor currentActorAccessor,
 15435        IOptions<DataAccessOptions> dataAccessOptions,
 15436        IApplicationAuditStore? auditStore = null,
 15437        IApplicationAuditContextAccessor? auditContextAccessor = null,
 15438        IApplicationAuditValuePolicy? auditValuePolicy = null,
 15439        IApplicationMutationManifestBuilder? manifestBuilder = null,
 15440        IApplicationMutationManifestHasher? manifestHasher = null)
 41    {
 15442        ArgumentNullException.ThrowIfNull(currentActorAccessor);
 15443        ArgumentNullException.ThrowIfNull(dataAccessOptions);
 44
 15445        _currentActorAccessor = currentActorAccessor;
 15446        _auditContextAccessor = auditContextAccessor;
 15447        _auditValuePolicy = auditValuePolicy ?? new DefaultApplicationAuditValuePolicy();
 15448        _manifestBuilder = manifestBuilder ?? new CanonicalApplicationMutationManifestBuilder();
 15449        _manifestHasher = manifestHasher ?? new Sha256ApplicationMutationManifestHasher();
 15450        _auditOptions = dataAccessOptions.Value.Auditing.Enabled;
 15451        _auditStore = ResolveAuditStore(dataAccessOptions.Value.Auditing, auditStore);
 15452    }
 53
 54    /// <inheritdoc />
 9055    public ApplicationMutationAuditReceipt? LastCompletedReceipt { get; private set; }
 56
 57    /// <inheritdoc />
 58    public bool ApplyBeforeSaveChanges(ApplicationDbContext dbContext)
 59    {
 1460        ArgumentNullException.ThrowIfNull(dbContext);
 1461        ResetActiveAuditState();
 62
 1463        IReadOnlyList<EntityEntry> entries = GetSavePipelineEntries(dbContext);
 1464        if (entries.Count == 0)
 65        {
 266            return false;
 67        }
 68
 1269        ApplyPersistedStringCanonicalization(entries);
 1270        ApplyLookupStringNormalization(entries);
 1271        ApplyTimestampNormalization(entries);
 1272        ApplyConcurrencyStamps(entries);
 73
 1274        if (_auditOptions)
 75        {
 476            _pendingAuditEntries = OnBeforeSaveChanges(dbContext, entries);
 77        }
 78
 1279        return true;
 80    }
 81
 82    /// <inheritdoc />
 83    public async ValueTask<bool> ApplyBeforeSaveChangesAsync(
 84        ApplicationDbContext dbContext,
 85        CancellationToken cancellationToken = default)
 86    {
 13487        ArgumentNullException.ThrowIfNull(dbContext);
 13488        cancellationToken.ThrowIfCancellationRequested();
 13489        ResetActiveAuditState();
 90
 13491        IReadOnlyList<EntityEntry> entries = GetSavePipelineEntries(dbContext);
 13492        if (entries.Count == 0)
 93        {
 294            return false;
 95        }
 96
 13297        ApplyPersistedStringCanonicalization(entries);
 13298        ApplyLookupStringNormalization(entries);
 13299        ApplyTimestampNormalization(entries);
 132100        ApplyConcurrencyStamps(entries);
 101
 132102        if (_auditOptions)
 103        {
 50104            _pendingAuditEntries = await OnBeforeSaveChangesAsync(
 50105                    dbContext,
 50106                    entries,
 50107                    cancellationToken)
 50108                .ConfigureAwait(false);
 109        }
 110
 132111        return true;
 134112    }
 113
 114    /// <inheritdoc />
 115    public bool ApplyAfterSaveChanges(ApplicationDbContext dbContext)
 116    {
 12117        ArgumentNullException.ThrowIfNull(dbContext);
 12118        bool appendedAdditionalAuditRecords = false;
 119
 24120        foreach (AuditEntry auditEntry in _pendingAuditEntries)
 121        {
 0122            CompleteTemporaryAuditProperties(auditEntry);
 0123            AppendAuditRecord(dbContext, auditEntry.ToAuditRecord());
 0124            appendedAdditionalAuditRecords = true;
 125        }
 126
 12127        CompleteMutationReceipt();
 12128        _pendingAuditEntries = [];
 12129        return appendedAdditionalAuditRecords;
 130    }
 131
 132    /// <inheritdoc />
 133    public async ValueTask<bool> ApplyAfterSaveChangesAsync(
 134        ApplicationDbContext dbContext,
 135        CancellationToken cancellationToken = default)
 136    {
 130137        ArgumentNullException.ThrowIfNull(dbContext);
 130138        cancellationToken.ThrowIfCancellationRequested();
 130139        bool appendedAdditionalAuditRecords = false;
 140
 266141        foreach (AuditEntry auditEntry in _pendingAuditEntries)
 142        {
 4143            CompleteTemporaryAuditProperties(auditEntry);
 4144            await AppendAuditRecordAsync(dbContext, auditEntry.ToAuditRecord(), cancellationToken)
 4145                .ConfigureAwait(false);
 2146            appendedAdditionalAuditRecords = true;
 147        }
 148
 128149        CompleteMutationReceipt();
 128150        _pendingAuditEntries = [];
 128151        return appendedAdditionalAuditRecords;
 128152    }
 153
 154    private void ResetActiveAuditState()
 155    {
 148156        _pendingAuditEntries = [];
 148157        _activeAuditRecords.Clear();
 148158        _activeAuditContext = null;
 148159        _activeMutationBatchId = null;
 148160        _activeAuditRecordCount = 0;
 148161    }
 162
 163    private static IReadOnlyList<EntityEntry> GetSavePipelineEntries(ApplicationDbContext dbContext)
 164    {
 148165        dbContext.ChangeTracker.DetectChanges();
 148166        return [.. dbContext.ChangeTracker
 148167            .Entries()
 322168            .Where(entry => entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)];
 169    }
 170
 171    private static void ApplyPersistedStringCanonicalization(IEnumerable<EntityEntry> entries)
 172    {
 616173        foreach (EntityEntry entry in entries)
 174        {
 164175            if (entry.Entity is AuditRecord ||
 164176                entry.State is not (EntityState.Added or EntityState.Modified))
 177            {
 178                continue;
 179            }
 180
 4532181            foreach (PropertyEntry property in entry.Properties)
 182            {
 2126183                if (property.Metadata.ClrType != typeof(string) ||
 2126184                    property.Metadata.IsPrimaryKey() ||
 2126185                    property.Metadata.IsConcurrencyToken ||
 2126186                    (entry.State == EntityState.Modified && !property.IsModified) ||
 2126187                    property.CurrentValue is not string currentValue)
 188                {
 189                    continue;
 190                }
 191
 760192                string canonicalValue = PersistenceStringCanonicalizer.Canonicalize(currentValue);
 760193                if (!string.Equals(canonicalValue, currentValue, StringComparison.Ordinal))
 194                {
 24195                    property.CurrentValue = canonicalValue;
 196                }
 197            }
 198        }
 144199    }
 200
 201    private static void ApplyLookupStringNormalization(IEnumerable<EntityEntry> entries)
 202    {
 616203        foreach (EntityEntry entry in entries)
 204        {
 164205            if (entry.Entity is not ExternalLoginAccount account ||
 164206                entry.State is not (EntityState.Added or EntityState.Modified))
 207            {
 208                continue;
 209            }
 210
 106211            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.ProviderName),
 106212                PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderName));
 106213            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.NormalizedProviderName),
 106214                PersistenceStringComparisonNormalizer.NormalizeRequiredLookupValue(account.ProviderName));
 106215            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.ProviderUserId),
 106216                PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderUserId));
 106217            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.DisplayName),
 106218                PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.DisplayName));
 106219            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.Email),
 106220                PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.Email));
 106221            SetPropertyCurrentValue(entry, nameof(ExternalLoginAccount.NormalizedEmail),
 106222                PersistenceStringComparisonNormalizer.NormalizeOptionalLookupValue(account.Email));
 223        }
 144224    }
 225
 226    private static void ApplyTimestampNormalization(IEnumerable<EntityEntry> entries)
 227    {
 616228        foreach (EntityEntry entry in entries)
 229        {
 164230            if (entry.State is not (EntityState.Added or EntityState.Modified))
 231            {
 232                continue;
 233            }
 234
 5544235            foreach (PropertyEntry property in entry.Properties)
 236            {
 2610237                if (!IsUtcTimestampProperty(property.Metadata.Name))
 238                {
 239                    continue;
 240                }
 241
 510242                Type propertyType = Nullable.GetUnderlyingType(property.Metadata.ClrType)
 510243                    ?? property.Metadata.ClrType;
 244
 510245                if (propertyType == typeof(DateTime) && property.CurrentValue is DateTime dateTimeValue)
 246                {
 236247                    property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeValue);
 248                }
 274249                else if (propertyType == typeof(DateTimeOffset) && property.CurrentValue is DateTimeOffset dateTimeOffse
 250                {
 0251                    property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeOffsetValue);
 252                }
 253            }
 254        }
 144255    }
 256
 257    private static void ApplyConcurrencyStamps(IEnumerable<EntityEntry> entries)
 258    {
 616259        foreach (EntityEntry entry in entries)
 260        {
 164261            if (entry.Entity is not DataEntity entity)
 262            {
 263                continue;
 264            }
 265
 164266            PropertyEntry concurrencyStampProperty = entry.Property(nameof(DataEntity.ConcurrencyStamp));
 164267            if (entry.State == EntityState.Added)
 268            {
 136269                if (string.IsNullOrWhiteSpace(entity.ConcurrencyStamp))
 270                {
 6271                    concurrencyStampProperty.CurrentValue = DataEntity.NewConcurrencyStamp();
 272                }
 273            }
 28274            else if (entry.State == EntityState.Modified)
 275            {
 26276                concurrencyStampProperty.CurrentValue = DataEntity.NewConcurrencyStamp();
 277            }
 278        }
 144279    }
 280
 281    private static bool IsUtcTimestampProperty(string propertyName)
 282    {
 2610283        return propertyName.EndsWith("Utc", StringComparison.Ordinal);
 284    }
 285
 286    private List<AuditEntry> OnBeforeSaveChanges(
 287        ApplicationDbContext dbContext,
 288        IEnumerable<EntityEntry> entries)
 289    {
 4290        List<AuditEntry> auditEntries = CreateAuditEntries(entries);
 20291        foreach (AuditEntry auditEntry in auditEntries.Where(entry => !entry.HasTemporaryProperties))
 292        {
 4293            AppendAuditRecord(dbContext, auditEntry.ToAuditRecord());
 294        }
 295
 8296        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    {
 50304        List<AuditEntry> auditEntries = CreateAuditEntries(entries);
 305        foreach (AuditEntry auditEntry in auditEntries.Where(entry => !entry.HasTemporaryProperties))
 306        {
 48307            await AppendAuditRecordAsync(dbContext, auditEntry.ToAuditRecord(), cancellationToken)
 48308                .ConfigureAwait(false);
 309        }
 310
 311        return [.. auditEntries.Where(entry => entry.HasTemporaryProperties)];
 50312    }
 313
 314    private void AppendAuditRecord(ApplicationDbContext dbContext, AuditRecord auditRecord)
 315    {
 4316        _auditStore.Append(dbContext, auditRecord);
 4317        _activeAuditRecords.Add(auditRecord);
 4318    }
 319
 320    private async ValueTask AppendAuditRecordAsync(
 321        ApplicationDbContext dbContext,
 322        AuditRecord auditRecord,
 323        CancellationToken cancellationToken)
 324    {
 52325        await _auditStore.AppendAsync(dbContext, auditRecord, cancellationToken).ConfigureAwait(false);
 50326        _activeAuditRecords.Add(auditRecord);
 50327    }
 328
 329    private List<AuditEntry> CreateAuditEntries(IEnumerable<EntityEntry> entries)
 330    {
 54331        var auditEntries = new List<AuditEntry>();
 54332        ApplicationAuditContext auditContext = ResolveAuditContext();
 54333        string mutationBatchId = Guid.NewGuid().ToString("N");
 334
 232335        foreach (EntityEntry entry in entries)
 336        {
 62337            if (entry.Entity is AuditRecord ||
 62338                entry.State == EntityState.Detached ||
 62339                entry.State == EntityState.Unchanged)
 340            {
 341                continue;
 342            }
 343
 56344            AuditEntry auditEntry = new(entry)
 56345            {
 56346                TableName = entry.Metadata.GetTableName() ?? string.Empty,
 56347                ModifiedBy = auditContext.ActorDisplayName,
 56348                ActorId = auditContext.ActorId,
 56349                ActorType = auditContext.ActorType,
 56350                MutationBatchId = mutationBatchId,
 56351                OperationExecutionId = auditContext.OperationExecutionId,
 56352                ExecutionAttemptId = auditContext.ExecutionAttemptId,
 56353                CorrelationId = auditContext.CorrelationId,
 56354                TraceId = auditContext.TraceId,
 56355                SpanId = auditContext.SpanId,
 56356                DecisionAuditRecordId = auditContext.DecisionAuditRecordId,
 56357                TenantHash = auditContext.TenantHash,
 56358                OrganizationHash = auditContext.OrganizationHash,
 56359                ModifiedOnUtc = PersistenceTimestamp.UtcNow(),
 56360                State = entry.State.ToString()
 56361            };
 56362            auditEntries.Add(auditEntry);
 363
 1464364            foreach (PropertyEntry property in entry.Properties)
 365            {
 676366                if (property.IsTemporary)
 367                {
 4368                    auditEntry.TemporaryProperties.Add(property);
 4369                    continue;
 370                }
 371
 672372                string propertyName = property.Metadata.Name;
 672373                if (property.Metadata.IsPrimaryKey())
 374                {
 52375                    AddProtectedValue(auditEntry.KeyValues, entry, propertyName, property.CurrentValue);
 52376                    continue;
 377                }
 378
 620379                switch (entry.State)
 380                {
 381                    case EntityState.Added:
 554382                        AddProtectedValue(auditEntry.CurrentValues, entry, propertyName, property.CurrentValue);
 554383                        break;
 384                    case EntityState.Deleted:
 22385                        AddProtectedValue(auditEntry.OriginalValues, entry, propertyName, property.OriginalValue);
 22386                        break;
 44387                    case EntityState.Modified when property.IsModified:
 10388                        AddProtectedValue(auditEntry.OriginalValues, entry, propertyName, property.OriginalValue);
 10389                        AddProtectedValue(auditEntry.CurrentValues, entry, propertyName, property.CurrentValue);
 10390                        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
 54401        if (auditEntries.Count > 0)
 402        {
 48403            _activeAuditContext = auditContext;
 48404            _activeMutationBatchId = mutationBatchId;
 48405            _activeAuditRecordCount = auditEntries.Count;
 406        }
 407
 54408        return auditEntries;
 409    }
 410
 411    private ApplicationAuditContext ResolveAuditContext()
 412    {
 54413        if (_auditContextAccessor is not null)
 414        {
 6415            return _auditContextAccessor.Current
 6416                ?? throw new InvalidOperationException("The application audit context accessor returned no context.");
 417        }
 418
 48419        string displayActor = _currentActorAccessor.CurrentActor;
 48420        string actorId = string.IsNullOrWhiteSpace(displayActor) ? "Unknown" : displayActor.Trim();
 48421        return new ApplicationAuditContext(
 48422            actorId,
 48423            actorId.Equals(SystemCurrentActorAccessor.ActorName, StringComparison.Ordinal)
 48424                ? ApplicationAuditActorTypes.System
 48425                : ApplicationAuditActorTypes.Unknown,
 48426            actorId);
 427    }
 428
 429    private void AddProtectedValue(
 430        Dictionary<string, object> target,
 431        EntityEntry entry,
 432        string propertyName,
 433        object? value)
 434    {
 652435        if (ApplicationAuditValueProtector.TryProtect(
 652436            _auditValuePolicy,
 652437            entry.Entity.GetType(),
 652438            propertyName,
 652439            value,
 652440            out object protectedValue))
 441        {
 648442            target[propertyName] = protectedValue;
 443        }
 652444    }
 445
 446    private static void SetPropertyCurrentValue(EntityEntry entry, string propertyName, object? value)
 447    {
 636448        entry.Property(propertyName).CurrentValue = value;
 636449    }
 450
 451    private void CompleteTemporaryAuditProperties(AuditEntry auditEntry)
 452    {
 16453        foreach (PropertyEntry property in auditEntry.TemporaryProperties)
 454        {
 4455            if (property.Metadata.IsPrimaryKey())
 456            {
 4457                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        }
 4464    }
 465
 466    private void CompleteMutationReceipt()
 467    {
 140468        if (_activeAuditContext is null ||
 140469            string.IsNullOrWhiteSpace(_activeMutationBatchId) ||
 140470            _activeAuditRecordCount == 0)
 471        {
 96472            return;
 473        }
 474
 44475        if (_activeAuditRecords.Count != _activeAuditRecordCount)
 476        {
 0477            throw new InvalidOperationException("The mutation audit receipt cannot be completed because the retained aud
 478        }
 479
 44480        ApplicationMutationManifest manifest = _manifestBuilder.Build(_activeAuditRecords);
 44481        string manifestHash = _manifestHasher.ComputeHash(manifest);
 482
 44483        LastCompletedReceipt = new ApplicationMutationAuditReceipt(
 44484            _activeMutationBatchId,
 44485            _activeAuditRecordCount,
 44486            "Committed",
 44487            DateTimeOffset.UtcNow,
 44488            manifestHash,
 44489            _manifestHasher.Algorithm,
 44490            manifest.SchemaVersion,
 44491            _activeAuditContext.OperationExecutionId,
 44492            _activeAuditContext.ExecutionAttemptId,
 44493            _activeAuditContext.DecisionAuditRecordId,
 44494            _activeAuditContext.CorrelationId,
 44495            _activeAuditContext.TraceId);
 496
 44497        _activeAuditContext = null;
 44498        _activeMutationBatchId = null;
 44499        _activeAuditRecordCount = 0;
 44500        _activeAuditRecords.Clear();
 44501    }
 502
 503    private static IApplicationAuditStore ResolveAuditStore(
 504        DataAuditingOptions auditingOptions,
 505        IApplicationAuditStore? auditStore)
 506    {
 154507        ArgumentNullException.ThrowIfNull(auditingOptions);
 154508        return auditStore is not null
 154509            ? auditStore
 154510            : !auditingOptions.Enabled || AuditStorageModes.IsLocal(auditingOptions.StorageMode)
 154511                ? new LocalApplicationAuditStore()
 154512                : throw new InvalidOperationException(
 154513                    $"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)
get_LastCompletedReceipt()
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)