| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.EntityFrameworkCore.ChangeTracking; |
| | | 3 | | using Microsoft.EntityFrameworkCore.Metadata; |
| | | 4 | | using Microsoft.Extensions.Logging; |
| | | 5 | | using Microsoft.Extensions.Options; |
| | | 6 | | using ProjectTemplate.Infrastructure.Data.Entities; |
| | | 7 | | using ProjectTemplate.Infrastructure.Data.Options; |
| | | 8 | | |
| | | 9 | | namespace ProjectTemplate.Infrastructure.Data; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Represents the EF Core database context for the ProjectTemplate application. |
| | | 13 | | /// </summary> |
| | | 14 | | public sealed partial class ApplicationDbContext( |
| | | 15 | | DbContextOptions<ApplicationDbContext> options, |
| | | 16 | | ILogger<ApplicationDbContext> logger, |
| | | 17 | | ICurrentActorAccessor currentActorAccessor, |
| | | 18 | | IOptions<DataAccessOptions> dataAccessOptions |
| | | 19 | | ) |
| | 100 | 20 | | : DbContext(options) |
| | | 21 | | { |
| | 100 | 22 | | private readonly ILogger<ApplicationDbContext> _logger = logger; |
| | 100 | 23 | | private readonly ICurrentActorAccessor _currentActorAccessor = currentActorAccessor; |
| | 100 | 24 | | private readonly bool _auditOptions = dataAccessOptions.Value.Auditing.Enabled; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Gets the audit records for the application. |
| | | 28 | | /// </summary> |
| | 44 | 29 | | public DbSet<AuditRecord> AuditRecords => Set<AuditRecord>(); |
| | | 30 | | /// <summary> |
| | | 31 | | /// Gets the external login account links for the application. |
| | | 32 | | /// </summary> |
| | 102 | 33 | | public DbSet<ExternalLoginAccount> ExternalLoginAccounts => Set<ExternalLoginAccount>(); |
| | | 34 | | |
| | | 35 | | [LoggerMessage( |
| | | 36 | | EventId = 19000, |
| | | 37 | | Level = LogLevel.Trace, |
| | | 38 | | Message = "{EfCoreMessage}")] |
| | | 39 | | private static partial void LogEfCoreMessage( |
| | | 40 | | ILogger logger, |
| | | 41 | | string efCoreMessage); |
| | | 42 | | |
| | | 43 | | [LoggerMessage( |
| | | 44 | | EventId = 19001, |
| | | 45 | | Level = LogLevel.Warning, |
| | | 46 | | Message = "Optimistic concurrency conflict detected while saving {EntryCount} tracked entity entries.")] |
| | | 47 | | private static partial void LogOptimisticConcurrencyConflict( |
| | | 48 | | ILogger logger, |
| | | 49 | | int entryCount, |
| | | 50 | | Exception exception); |
| | | 51 | | |
| | | 52 | | /// <inheritdoc /> |
| | | 53 | | protected override void OnModelCreating(ModelBuilder modelBuilder) |
| | | 54 | | { |
| | 6 | 55 | | ArgumentNullException.ThrowIfNull(modelBuilder); |
| | | 56 | | |
| | 6 | 57 | | modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly); |
| | 6 | 58 | | ConfigureDataEntityDefaults(modelBuilder); |
| | 6 | 59 | | ConfigureTimestampDefaults(modelBuilder); |
| | | 60 | | |
| | 6 | 61 | | base.OnModelCreating(modelBuilder); |
| | 6 | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <inheritdoc /> |
| | | 65 | | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) |
| | | 66 | | { |
| | 100 | 67 | | ArgumentNullException.ThrowIfNull(optionsBuilder); |
| | | 68 | | |
| | 100 | 69 | | optionsBuilder |
| | 6078 | 70 | | .LogTo(message => LogEfCoreMessage(_logger, message), LogLevel.Trace) |
| | 100 | 71 | | .EnableDetailedErrors(); |
| | | 72 | | |
| | 100 | 73 | | base.OnConfiguring(optionsBuilder); |
| | 100 | 74 | | } |
| | | 75 | | |
| | | 76 | | public bool HasUnsavedChanges() |
| | | 77 | | { |
| | 0 | 78 | | return ChangeTracker.HasChanges(); |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | public override int SaveChanges() |
| | | 82 | | { |
| | 10 | 83 | | return SaveChanges(acceptAllChangesOnSuccess: true); |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | public override int SaveChanges(bool acceptAllChangesOnSuccess = true) |
| | | 87 | | { |
| | 10 | 88 | | ApplyPersistedStringCanonicalization(); |
| | 10 | 89 | | ApplyLookupStringNormalization(); |
| | 10 | 90 | | ApplyTimestampNormalization(); |
| | 10 | 91 | | ApplyConcurrencyStamps(); |
| | | 92 | | |
| | 10 | 93 | | if (!_auditOptions) |
| | | 94 | | { |
| | 8 | 95 | | return SaveChangesWithConcurrencyHandling( |
| | 16 | 96 | | () => base.SaveChanges(acceptAllChangesOnSuccess)); |
| | | 97 | | } |
| | | 98 | | |
| | 2 | 99 | | List<AuditEntry> auditEntries = OnBeforeSaveChanges(); |
| | | 100 | | |
| | 2 | 101 | | int result = SaveChangesWithConcurrencyHandling( |
| | 4 | 102 | | () => base.SaveChanges(acceptAllChangesOnSuccess)); |
| | | 103 | | |
| | 2 | 104 | | _ = OnAfterSaveChanges(auditEntries); |
| | | 105 | | |
| | 2 | 106 | | return result; |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) |
| | | 110 | | { |
| | 62 | 111 | | return SaveChangesAsync( |
| | 62 | 112 | | acceptAllChangesOnSuccess: true, |
| | 62 | 113 | | cancellationToken); |
| | | 114 | | } |
| | | 115 | | |
| | | 116 | | public override async Task<int> SaveChangesAsync( |
| | | 117 | | bool acceptAllChangesOnSuccess, |
| | | 118 | | CancellationToken cancellationToken = default) |
| | | 119 | | { |
| | 62 | 120 | | ApplyPersistedStringCanonicalization(); |
| | 62 | 121 | | ApplyLookupStringNormalization(); |
| | 62 | 122 | | ApplyTimestampNormalization(); |
| | 62 | 123 | | ApplyConcurrencyStamps(); |
| | | 124 | | |
| | 62 | 125 | | if (!_auditOptions) |
| | | 126 | | { |
| | 42 | 127 | | return await SaveChangesWithConcurrencyHandlingAsync( |
| | 84 | 128 | | () => base.SaveChangesAsync( |
| | 84 | 129 | | acceptAllChangesOnSuccess, |
| | 84 | 130 | | cancellationToken)).ConfigureAwait(false); |
| | | 131 | | } |
| | | 132 | | |
| | 20 | 133 | | List<AuditEntry> auditEntries = OnBeforeSaveChanges(); |
| | | 134 | | |
| | 20 | 135 | | int result = await SaveChangesWithConcurrencyHandlingAsync( |
| | 40 | 136 | | () => base.SaveChangesAsync( |
| | 40 | 137 | | acceptAllChangesOnSuccess, |
| | 40 | 138 | | cancellationToken)).ConfigureAwait(false); |
| | | 139 | | |
| | 18 | 140 | | _ = await OnAfterSaveChangesAsync(auditEntries, cancellationToken) |
| | 18 | 141 | | .ConfigureAwait(false); |
| | | 142 | | |
| | 18 | 143 | | return result; |
| | 58 | 144 | | } |
| | | 145 | | |
| | | 146 | | private void ApplyPersistedStringCanonicalization() |
| | | 147 | | { |
| | 72 | 148 | | ChangeTracker.DetectChanges(); |
| | | 149 | | |
| | 300 | 150 | | foreach (EntityEntry entry in ChangeTracker.Entries()) |
| | | 151 | | { |
| | 78 | 152 | | if (entry.Entity is AuditRecord || |
| | 78 | 153 | | entry.State is not (EntityState.Added or EntityState.Modified)) |
| | | 154 | | { |
| | | 155 | | continue; |
| | | 156 | | } |
| | | 157 | | |
| | 1872 | 158 | | foreach (PropertyEntry property in entry.Properties) |
| | | 159 | | { |
| | 864 | 160 | | if (property.Metadata.ClrType != typeof(string) || |
| | 864 | 161 | | property.Metadata.IsPrimaryKey() || |
| | 864 | 162 | | property.Metadata.IsConcurrencyToken) |
| | | 163 | | { |
| | | 164 | | continue; |
| | | 165 | | } |
| | | 166 | | |
| | 432 | 167 | | if (entry.State == EntityState.Modified && !property.IsModified) |
| | | 168 | | { |
| | | 169 | | continue; |
| | | 170 | | } |
| | | 171 | | |
| | 352 | 172 | | if (property.CurrentValue is not string currentValue) |
| | | 173 | | { |
| | | 174 | | continue; |
| | | 175 | | } |
| | | 176 | | |
| | 294 | 177 | | string canonicalValue = PersistenceStringCanonicalizer.Canonicalize(currentValue); |
| | | 178 | | |
| | 294 | 179 | | if (!string.Equals(canonicalValue, currentValue, StringComparison.Ordinal)) |
| | | 180 | | { |
| | 24 | 181 | | property.CurrentValue = canonicalValue; |
| | | 182 | | } |
| | | 183 | | } |
| | | 184 | | } |
| | 72 | 185 | | } |
| | | 186 | | |
| | | 187 | | private void ApplyLookupStringNormalization() |
| | | 188 | | { |
| | 72 | 189 | | ChangeTracker.DetectChanges(); |
| | | 190 | | |
| | 296 | 191 | | foreach (EntityEntry<ExternalLoginAccount> entry in ChangeTracker.Entries<ExternalLoginAccount>()) |
| | | 192 | | { |
| | 76 | 193 | | if (entry.State is not (EntityState.Added or EntityState.Modified)) |
| | | 194 | | { |
| | | 195 | | continue; |
| | | 196 | | } |
| | | 197 | | |
| | 72 | 198 | | ExternalLoginAccount account = entry.Entity; |
| | | 199 | | |
| | 72 | 200 | | account.ProviderName = |
| | 72 | 201 | | PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderName); |
| | | 202 | | |
| | 72 | 203 | | account.NormalizedProviderName = |
| | 72 | 204 | | PersistenceStringComparisonNormalizer.NormalizeRequiredLookupValue(account.ProviderName); |
| | | 205 | | |
| | 72 | 206 | | account.ProviderUserId = |
| | 72 | 207 | | PersistenceStringComparisonNormalizer.NormalizeRequiredDisplayValue(account.ProviderUserId); |
| | | 208 | | |
| | 72 | 209 | | account.DisplayName = |
| | 72 | 210 | | PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.DisplayName); |
| | | 211 | | |
| | 72 | 212 | | account.Email = |
| | 72 | 213 | | PersistenceStringComparisonNormalizer.NormalizeOptionalDisplayValue(account.Email); |
| | | 214 | | |
| | 72 | 215 | | account.NormalizedEmail = |
| | 72 | 216 | | PersistenceStringComparisonNormalizer.NormalizeOptionalLookupValue(account.Email); |
| | | 217 | | } |
| | 72 | 218 | | } |
| | | 219 | | |
| | | 220 | | private void ApplyTimestampNormalization() |
| | | 221 | | { |
| | 72 | 222 | | ChangeTracker.DetectChanges(); |
| | | 223 | | |
| | 300 | 224 | | foreach (EntityEntry entry in ChangeTracker.Entries()) |
| | | 225 | | { |
| | 78 | 226 | | if (entry.State is not (EntityState.Added or EntityState.Modified)) |
| | | 227 | | { |
| | | 228 | | continue; |
| | | 229 | | } |
| | | 230 | | |
| | 1916 | 231 | | foreach (PropertyEntry property in entry.Properties) |
| | | 232 | | { |
| | 884 | 233 | | if (!IsUtcTimestampProperty(property.Metadata.Name)) |
| | | 234 | | { |
| | | 235 | | continue; |
| | | 236 | | } |
| | | 237 | | |
| | 218 | 238 | | Type propertyType = Nullable.GetUnderlyingType(property.Metadata.ClrType) |
| | 218 | 239 | | ?? property.Metadata.ClrType; |
| | | 240 | | |
| | 218 | 241 | | if (propertyType == typeof(DateTime) && |
| | 218 | 242 | | property.CurrentValue is DateTime dateTimeValue) |
| | | 243 | | { |
| | 84 | 244 | | property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeValue); |
| | 84 | 245 | | continue; |
| | | 246 | | } |
| | | 247 | | |
| | 134 | 248 | | if (propertyType == typeof(DateTimeOffset) && |
| | 134 | 249 | | property.CurrentValue is DateTimeOffset dateTimeOffsetValue) |
| | | 250 | | { |
| | 0 | 251 | | property.CurrentValue = PersistenceTimestamp.NormalizeUtc(dateTimeOffsetValue); |
| | | 252 | | } |
| | | 253 | | } |
| | | 254 | | } |
| | 72 | 255 | | } |
| | | 256 | | |
| | | 257 | | private static bool IsUtcTimestampProperty(string propertyName) |
| | | 258 | | { |
| | 908 | 259 | | return propertyName.EndsWith("Utc", StringComparison.Ordinal); |
| | | 260 | | } |
| | | 261 | | private List<AuditEntry> OnBeforeSaveChanges() |
| | | 262 | | { |
| | 22 | 263 | | ChangeTracker.DetectChanges(); |
| | 22 | 264 | | var auditEntries = new List<AuditEntry>(); |
| | 100 | 265 | | foreach (EntityEntry entry in ChangeTracker.Entries()) |
| | | 266 | | { |
| | 28 | 267 | | if (entry.Entity is AuditRecord || entry.State == EntityState.Detached || entry.State == EntityState.Unchang |
| | | 268 | | { |
| | | 269 | | continue; |
| | | 270 | | } |
| | | 271 | | |
| | 24 | 272 | | AuditEntry auditEntry = new(entry) |
| | 24 | 273 | | { |
| | 24 | 274 | | TableName = entry.Metadata.GetTableName() ?? string.Empty, |
| | 24 | 275 | | ModifiedBy = _currentActorAccessor.CurrentActor, |
| | 24 | 276 | | ModifiedOnUtc = PersistenceTimestamp.UtcNow(), |
| | 24 | 277 | | State = entry.State.ToString() |
| | 24 | 278 | | }; |
| | 24 | 279 | | auditEntries.Add(auditEntry); |
| | | 280 | | |
| | 624 | 281 | | foreach (PropertyEntry property in entry.Properties) |
| | | 282 | | { |
| | 288 | 283 | | if (property.IsTemporary) |
| | | 284 | | { |
| | | 285 | | // value will be generated by the database, get the value after saving |
| | 0 | 286 | | auditEntry.TemporaryProperties.Add(property); |
| | 0 | 287 | | continue; |
| | | 288 | | } |
| | | 289 | | |
| | 288 | 290 | | string propertyName = property.Metadata.Name; |
| | 288 | 291 | | if (property.Metadata.IsPrimaryKey()) |
| | | 292 | | { |
| | 24 | 293 | | auditEntry.KeyValues[propertyName] = property.CurrentValue ?? string.Empty; |
| | 24 | 294 | | continue; |
| | | 295 | | } |
| | | 296 | | |
| | 264 | 297 | | switch (entry.State) |
| | | 298 | | { |
| | | 299 | | case EntityState.Added: |
| | 198 | 300 | | auditEntry.CurrentValues[propertyName] = property.CurrentValue ?? string.Empty; |
| | 198 | 301 | | break; |
| | | 302 | | |
| | | 303 | | case EntityState.Deleted: |
| | 22 | 304 | | auditEntry.OriginalValues[propertyName] = property.OriginalValue ?? string.Empty; |
| | 22 | 305 | | break; |
| | | 306 | | |
| | | 307 | | case EntityState.Modified: |
| | 44 | 308 | | if (property.IsModified) |
| | | 309 | | { |
| | 10 | 310 | | auditEntry.OriginalValues[propertyName] = property.OriginalValue ?? string.Empty; |
| | 10 | 311 | | auditEntry.CurrentValues[propertyName] = property.CurrentValue ?? string.Empty; |
| | | 312 | | } |
| | | 313 | | break; |
| | | 314 | | case EntityState.Detached: |
| | | 315 | | case EntityState.Unchanged: |
| | | 316 | | default: |
| | | 317 | | break; |
| | | 318 | | } |
| | | 319 | | } |
| | | 320 | | } |
| | | 321 | | |
| | | 322 | | // Save audit entities that have all the modifications |
| | 116 | 323 | | foreach (AuditEntry auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties)) |
| | | 324 | | { |
| | 24 | 325 | | AuditRecords.Add(auditEntry.ToAuditRecord()); |
| | | 326 | | } |
| | | 327 | | |
| | | 328 | | // keep a list of entries where the value of some properties are unknown at this step |
| | 46 | 329 | | return [.. auditEntries.Where(_ => _.HasTemporaryProperties)]; |
| | | 330 | | } |
| | | 331 | | |
| | | 332 | | [System.Diagnostics.CodeAnalysis.SuppressMessage( |
| | | 333 | | "Style", |
| | | 334 | | "IDE0002:Simplify Member Access", |
| | | 335 | | Justification = "The base call intentionally bypasses the overridden SaveChanges audit pipeline.")] |
| | | 336 | | private int OnAfterSaveChanges(List<AuditEntry> auditEntries) |
| | | 337 | | { |
| | 2 | 338 | | if (auditEntries == null || auditEntries.Count == 0) |
| | | 339 | | { |
| | 2 | 340 | | return 0; |
| | | 341 | | } |
| | | 342 | | |
| | 0 | 343 | | foreach (AuditEntry auditEntry in auditEntries) |
| | | 344 | | { |
| | | 345 | | // Get the final value of the temporary properties |
| | 0 | 346 | | foreach (PropertyEntry prop in auditEntry.TemporaryProperties) |
| | | 347 | | { |
| | 0 | 348 | | if (prop.Metadata.IsPrimaryKey()) |
| | | 349 | | { |
| | 0 | 350 | | auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty; |
| | | 351 | | } |
| | | 352 | | else |
| | | 353 | | { |
| | 0 | 354 | | auditEntry.CurrentValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty; |
| | | 355 | | } |
| | | 356 | | } |
| | | 357 | | |
| | | 358 | | // Save the audit entry. |
| | 0 | 359 | | AuditRecords.Add(auditEntry.ToAuditRecord()); |
| | | 360 | | |
| | | 361 | | } |
| | | 362 | | |
| | 0 | 363 | | return base.SaveChanges(); |
| | | 364 | | } |
| | | 365 | | |
| | | 366 | | [System.Diagnostics.CodeAnalysis.SuppressMessage( |
| | | 367 | | "Style", |
| | | 368 | | "IDE0002:Simplify Member Access", |
| | | 369 | | Justification = "The base call intentionally bypasses the overridden SaveChanges audit pipeline.")] |
| | | 370 | | private async Task<int> OnAfterSaveChangesAsync( |
| | | 371 | | List<AuditEntry> auditEntries, |
| | | 372 | | CancellationToken cancellationToken) |
| | | 373 | | { |
| | 18 | 374 | | if (auditEntries == null || auditEntries.Count == 0) |
| | | 375 | | { |
| | 18 | 376 | | return 0; |
| | | 377 | | } |
| | | 378 | | |
| | 0 | 379 | | foreach (AuditEntry auditEntry in auditEntries) |
| | | 380 | | { |
| | | 381 | | // Get the final value of the temporary properties |
| | 0 | 382 | | foreach (PropertyEntry prop in auditEntry.TemporaryProperties) |
| | | 383 | | { |
| | 0 | 384 | | if (prop.Metadata.IsPrimaryKey()) |
| | | 385 | | { |
| | 0 | 386 | | auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty; |
| | | 387 | | } |
| | | 388 | | else |
| | | 389 | | { |
| | 0 | 390 | | auditEntry.CurrentValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty; |
| | | 391 | | } |
| | | 392 | | } |
| | | 393 | | |
| | | 394 | | // Save the audit entry. |
| | 0 | 395 | | await AuditRecords |
| | 0 | 396 | | .AddAsync(auditEntry.ToAuditRecord(), cancellationToken) |
| | 0 | 397 | | .ConfigureAwait(false); |
| | | 398 | | } |
| | | 399 | | |
| | 0 | 400 | | return await base |
| | 0 | 401 | | .SaveChangesAsync(cancellationToken) |
| | 0 | 402 | | .ConfigureAwait(false); |
| | 18 | 403 | | } |
| | | 404 | | |
| | | 405 | | private static void ConfigureDataEntityDefaults(ModelBuilder modelBuilder) |
| | | 406 | | { |
| | 36 | 407 | | foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) |
| | | 408 | | { |
| | 12 | 409 | | if (!typeof(DataEntity).IsAssignableFrom(entityType.ClrType)) |
| | | 410 | | { |
| | | 411 | | continue; |
| | | 412 | | } |
| | | 413 | | |
| | 12 | 414 | | modelBuilder.Entity(entityType.ClrType) |
| | 12 | 415 | | .Property<string>(nameof(DataEntity.ConcurrencyStamp)) |
| | 12 | 416 | | .HasMaxLength(64) |
| | 12 | 417 | | .IsRequired() |
| | 12 | 418 | | .IsConcurrencyToken(); |
| | | 419 | | } |
| | 6 | 420 | | } |
| | | 421 | | |
| | | 422 | | private static void ConfigureTimestampDefaults(ModelBuilder modelBuilder) |
| | | 423 | | { |
| | 36 | 424 | | foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) |
| | | 425 | | { |
| | 288 | 426 | | foreach (IMutableProperty property in entityType.GetProperties()) |
| | | 427 | | { |
| | 132 | 428 | | Type propertyType = Nullable.GetUnderlyingType(property.ClrType) |
| | 132 | 429 | | ?? property.ClrType; |
| | | 430 | | |
| | 132 | 431 | | if ((propertyType == typeof(DateTime) || propertyType == typeof(DateTimeOffset)) && |
| | 132 | 432 | | IsUtcTimestampProperty(property.Name)) |
| | | 433 | | { |
| | 24 | 434 | | property.SetPrecision(PersistenceTimestamp.Precision); |
| | | 435 | | } |
| | | 436 | | } |
| | | 437 | | } |
| | 6 | 438 | | } |
| | | 439 | | |
| | | 440 | | private void ApplyConcurrencyStamps() |
| | | 441 | | { |
| | 72 | 442 | | ChangeTracker.DetectChanges(); |
| | | 443 | | |
| | 300 | 444 | | foreach (EntityEntry<DataEntity> entry in ChangeTracker.Entries<DataEntity>()) |
| | | 445 | | { |
| | 78 | 446 | | if (entry.State == EntityState.Added) |
| | | 447 | | { |
| | 58 | 448 | | if (string.IsNullOrWhiteSpace(entry.Entity.ConcurrencyStamp)) |
| | | 449 | | { |
| | 6 | 450 | | entry.Entity.ConcurrencyStamp = DataEntity.NewConcurrencyStamp(); |
| | | 451 | | } |
| | | 452 | | |
| | 6 | 453 | | continue; |
| | | 454 | | } |
| | | 455 | | |
| | 20 | 456 | | if (entry.State == EntityState.Modified) |
| | | 457 | | { |
| | 16 | 458 | | entry.Entity.ConcurrencyStamp = DataEntity.NewConcurrencyStamp(); |
| | | 459 | | } |
| | | 460 | | } |
| | 72 | 461 | | } |
| | | 462 | | |
| | | 463 | | private int SaveChangesWithConcurrencyHandling(Func<int> saveChanges) |
| | | 464 | | { |
| | | 465 | | try |
| | | 466 | | { |
| | 10 | 467 | | return saveChanges(); |
| | | 468 | | } |
| | 2 | 469 | | catch (DbUpdateConcurrencyException exception) |
| | | 470 | | { |
| | 2 | 471 | | LogOptimisticConcurrencyConflict(_logger, exception.Entries.Count, exception); |
| | 2 | 472 | | throw; |
| | | 473 | | } |
| | 8 | 474 | | } |
| | | 475 | | |
| | | 476 | | private async Task<int> SaveChangesWithConcurrencyHandlingAsync(Func<Task<int>> saveChanges) |
| | | 477 | | { |
| | | 478 | | try |
| | | 479 | | { |
| | 62 | 480 | | return await saveChanges().ConfigureAwait(false); |
| | | 481 | | } |
| | 2 | 482 | | catch (DbUpdateConcurrencyException exception) |
| | | 483 | | { |
| | 2 | 484 | | LogOptimisticConcurrencyConflict(_logger, exception.Entries.Count, exception); |
| | 2 | 485 | | throw; |
| | | 486 | | } |
| | 58 | 487 | | } |
| | | 488 | | } |