From 047cf71945b60839d4de544041f42f1e6ff884f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20J=C3=B8rgen=20Skogstad?= Date: Mon, 12 Feb 2024 15:08:55 +0100 Subject: [PATCH] fix: 412 status on multiple requests without revision header (#427) --- .../Externals/IDialogDbContext.cs | 1 - .../Externals/IUnitOfWork.cs | 6 ++ .../Dialogs/Queries/Get/GetDialogDto.cs | 2 +- .../Dialogs/Queries/Get/MappingProfile.cs | 1 + .../Commands/Delete/DeleteDialogCommand.cs | 8 +- .../Commands/Update/UpdateDialogCommand.cs | 8 +- .../Dialogs/Queries/Get/GetDialogDto.cs | 2 +- .../Dialogs/Queries/Get/MappingProfile.cs | 1 + .../Entities/Activities/DialogActivity.cs | 12 ++- .../Dialogs/Entities/DialogEntity.cs | 7 +- .../Entities/Elements/DialogElement.cs | 24 +++--- .../OptimisticConcurrencyTimeoutException.cs | 3 + ...DomainEventsToOutboxMessagesInterceptor.cs | 3 +- .../Persistence/DialogDbContext.cs | 2 +- .../UnitOfWork.cs | 80 +++++++++++++++++++ .../V1/EndUser/Dialogs/GetDialogEndpoint.cs | 2 +- .../CreateDialogActivityEndpoint.cs | 6 +- .../CreateDialogElementEndpoint.cs | 6 +- .../DeleteDialogElementEndpoint.cs | 4 +- .../UpdateDialogElementEndpoint.cs | 6 +- .../Dialogs/DeleteDialogEndpoint.cs | 4 +- .../ServiceOwner/Dialogs/GetDialogEndpoint.cs | 2 +- .../Dialogs/PatchDialogsController.cs | 2 +- .../Dialogs/UpdateDialogEndpoint.cs | 6 +- .../EventPublisher/IEventPublisher.cs | 7 +- .../EventPublisherExtensions.cs | 11 +-- tests/k6/run.ps1 | 4 +- tests/k6/run.sh | 4 +- ...te_alltests.ps1 => generate_all_tests.ps1} | 0 ...rate_alltests.sh => generate_all_tests.sh} | 0 tests/k6/tests/serviceowner/concurrency.js | 2 +- 31 files changed, 164 insertions(+), 62 deletions(-) create mode 100644 src/Digdir.Domain.Dialogporten.Infrastructure/Common/Exceptions/OptimisticConcurrencyTimeoutException.cs mode change 100644 => 100755 tests/k6/run.sh rename tests/k6/scripts/{generate_alltests.ps1 => generate_all_tests.ps1} (100%) rename tests/k6/scripts/{generate_alltests.sh => generate_all_tests.sh} (100%) mode change 100644 => 100755 diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/IDialogDbContext.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/IDialogDbContext.cs index 4f87b68ed..7a7af6066 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/IDialogDbContext.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/IDialogDbContext.cs @@ -48,5 +48,4 @@ Task> GetExistingIds( IEnumerable entities, CancellationToken cancellationToken) where TEntity : class, IIdentifiableEntity; - bool TrySetOriginalRevision(TEntity entity, Guid? revision) where TEntity : class, IVersionableEntity; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/IUnitOfWork.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/IUnitOfWork.cs index 87e09b19c..222f2d3e6 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Externals/IUnitOfWork.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/IUnitOfWork.cs @@ -1,4 +1,5 @@ using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; +using Digdir.Library.Entity.Abstractions.Features.Versionable; using OneOf.Types; using OneOf; @@ -8,6 +9,11 @@ public interface IUnitOfWork { IUnitOfWork WithoutAuditableSideEffects(); Task SaveChangesAsync(CancellationToken cancellationToken = default); + + IUnitOfWork EnableConcurrencyCheck( + TEntity? entity, + Guid? revision) + where TEntity : class, IVersionableEntity; } [GenerateOneOf] diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs index d866f6bc4..0958e4f20 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs @@ -11,7 +11,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Que public sealed class GetDialogDto { public Guid Id { get; set; } - public Guid Revision { get; set; } + public Guid IfMatchDialogRevision { get; set; } public string Org { get; set; } = null!; public string ServiceResource { get; set; } = null!; public string Party { get; set; } = null!; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs index cdf6ae997..b3ccd1d09 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs @@ -12,6 +12,7 @@ internal sealed class MappingProfile : Profile public MappingProfile() { CreateMap() + .ForMember(dest => dest.IfMatchDialogRevision, opt => opt.MapFrom(src => src.Revision)) .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusId)); CreateMap() diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs index c9d14c3ec..59ca1ecc5 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Delete/DeleteDialogCommand.cs @@ -14,7 +14,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog public sealed class DeleteDialogCommand : IRequest { public Guid Id { get; set; } - public Guid? Revision { get; set; } + public Guid? IfMatchDialogRevision { get; set; } } [GenerateOneOf] @@ -51,9 +51,11 @@ public async Task Handle(DeleteDialogCommand request, Cancel return new EntityNotFound(request.Id); } - _db.TrySetOriginalRevision(dialog, request.Revision); _db.Dialogs.SoftRemove(dialog); - var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken); + var saveResult = await _unitOfWork + .EnableConcurrencyCheck(dialog, request.IfMatchDialogRevision) + .SaveChangesAsync(cancellationToken); + return saveResult.Match( success => success, domainError => throw new UnreachableException("Should never get a domain error when creating a new dialog"), diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs index 4588a6add..bd38658bc 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Update/UpdateDialogCommand.cs @@ -19,7 +19,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog public sealed class UpdateDialogCommand : IRequest { public Guid Id { get; set; } - public Guid? Revision { get; set; } + public Guid? IfMatchDialogRevision { get; set; } public UpdateDialogDto Dto { get; set; } = null!; } @@ -75,8 +75,6 @@ public async Task Handle(UpdateDialogCommand request, Cancel return new EntityNotFound(request.Id); } - _db.TrySetOriginalRevision(dialog, request.Revision); - // Update primitive properties _mapper.Map(request.Dto, dialog); ValidateTimeFields(dialog); @@ -123,8 +121,10 @@ await dialog.Elements update: UpdateApiActions, delete: DeleteDelegate.NoOp); + var saveResult = await _unitOfWork + .EnableConcurrencyCheck(dialog, request.IfMatchDialogRevision) + .SaveChangesAsync(cancellationToken); - var saveResult = await _unitOfWork.SaveChangesAsync(cancellationToken); return saveResult.Match( success => success, domainError => domainError, diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs index 3b5fa559c..5337706e6 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs @@ -11,7 +11,7 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialog public sealed class GetDialogDto { public Guid Id { get; set; } - public Guid Revision { get; set; } + public Guid IfMatchDialogRevision { get; set; } public string Org { get; set; } = null!; public string ServiceResource { get; set; } = null!; public string Party { get; set; } = null!; diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/MappingProfile.cs index ff863efc4..af5635759 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/MappingProfile.cs @@ -12,6 +12,7 @@ internal sealed class MappingProfile : Profile public MappingProfile() { CreateMap() + .ForMember(dest => dest.IfMatchDialogRevision, opt => opt.MapFrom(src => src.Revision)) .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusId)); CreateMap() diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs index 7f54f6a76..5e1cde17c 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs @@ -34,15 +34,21 @@ public class DialogActivity : IImmutableEntity, IAggregateCreatedHandler, IEvent [AggregateChild] public DialogActivityPerformedBy? PerformedBy { get; set; } - public List RelatedActivities { get; set; } = new(); + public List RelatedActivities { get; set; } = []; public void OnCreate(AggregateNode self, DateTimeOffset utcNow) { _domainEvents.Add(new DialogActivityCreatedDomainEvent(DialogId, Id)); } - private readonly List _domainEvents = new(); - public IReadOnlyCollection DomainEvents => _domainEvents; + private readonly List _domainEvents = []; + + public IEnumerable PopDomainEvents() + { + var events = _domainEvents.ToList(); + _domainEvents.Clear(); + return events; + } } public class DialogActivityDescription : LocalizationSet diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs index 05c417472..3aaec1f3c 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs @@ -128,5 +128,10 @@ public void UpdateSeenAt(string seenByEndUserId, string? seenByEndUserName) } private readonly List _domainEvents = []; - public IReadOnlyCollection DomainEvents => _domainEvents; + public IEnumerable PopDomainEvents() + { + var events = _domainEvents.ToList(); + _domainEvents.Clear(); + return events; + } } diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Elements/DialogElement.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Elements/DialogElement.cs index c2dd14914..bf37fec40 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Elements/DialogElement.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Elements/DialogElement.cs @@ -27,13 +27,11 @@ public class DialogElement : IEntity, IAggregateChangedHandler, IEventPublisher public DialogElement? RelatedDialogElement { get; set; } // === Principal relationships === - [AggregateChild] - public DialogElementDisplayName? DisplayName { get; set; } - [AggregateChild] - public List Urls { get; set; } = new(); - public List ApiActions { get; set; } = new(); - public List Activities { get; set; } = new(); - public List RelatedDialogElements { get; set; } = new(); + [AggregateChild] public DialogElementDisplayName? DisplayName { get; set; } + [AggregateChild] public List Urls { get; set; } = []; + public List ApiActions { get; set; } = []; + public List Activities { get; set; } = []; + public List RelatedDialogElements { get; set; } = []; public void OnCreate(AggregateNode self, DateTimeOffset utcNow) { @@ -50,13 +48,19 @@ public void OnDelete(AggregateNode self, DateTimeOffset utcNow) _domainEvents.Add(new DialogElementDeletedDomainEvent(DialogId, Id, RelatedDialogElementId, Type)); } - public IReadOnlyCollection DomainEvents => _domainEvents; - private readonly List _domainEvents = new(); - public void SoftDelete() { _domainEvents.Add(new DialogElementDeletedDomainEvent(DialogId, Id, RelatedDialogElementId, Type)); } + + private readonly List _domainEvents = []; + + public IEnumerable PopDomainEvents() + { + var events = _domainEvents.ToList(); + _domainEvents.Clear(); + return events; + } } public class DialogElementDisplayName : LocalizationSet diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Exceptions/OptimisticConcurrencyTimeoutException.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Exceptions/OptimisticConcurrencyTimeoutException.cs new file mode 100644 index 000000000..4d8c8ce5f --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Exceptions/OptimisticConcurrencyTimeoutException.cs @@ -0,0 +1,3 @@ +namespace Digdir.Domain.Dialogporten.Infrastructure.Common.Exceptions; + +internal class OptimisticConcurrencyTimeoutException : Exception; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/DomainEvents/Outbox/ConvertDomainEventsToOutboxMessagesInterceptor.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/DomainEvents/Outbox/ConvertDomainEventsToOutboxMessagesInterceptor.cs index cd3ab4563..5f0b54880 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/DomainEvents/Outbox/ConvertDomainEventsToOutboxMessagesInterceptor.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/DomainEvents/Outbox/ConvertDomainEventsToOutboxMessagesInterceptor.cs @@ -29,7 +29,7 @@ public override ValueTask> SavingChangesAsync( var domainEvents = dbContext.ChangeTracker.Entries() .SelectMany(x => x.Entity is IEventPublisher publisher - ? publisher.DomainEvents + ? publisher.PopDomainEvents() : Enumerable.Empty()) .ToList(); @@ -43,6 +43,7 @@ x.Entity is IEventPublisher publisher .ToList(); dbContext.Set().AddRange(outboxMessages); + return base.SavingChangesAsync(eventData, result, cancellationToken); } } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/DialogDbContext.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/DialogDbContext.cs index 488bdcfbc..f6d586665 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/DialogDbContext.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/DialogDbContext.cs @@ -36,7 +36,7 @@ public DialogDbContext(DbContextOptions options) : base(options public DbSet OutboxMessages => Set(); public DbSet OutboxMessageConsumers => Set(); - public bool TrySetOriginalRevision( + internal bool TrySetOriginalRevision( TEntity? entity, Guid? revision) where TEntity : class, IVersionableEntity diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs index 40df9340c..88c7ecf97 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/UnitOfWork.cs @@ -1,20 +1,29 @@ using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; +using Digdir.Domain.Dialogporten.Infrastructure.Common.Exceptions; using Digdir.Domain.Dialogporten.Infrastructure.Persistence; +using Digdir.Library.Entity.Abstractions.Features.Versionable; using Digdir.Library.Entity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using OneOf.Types; +using Polly; +using Polly.Contrib.WaitAndRetry; +using Polly.Timeout; +using Polly.Wrap; namespace Digdir.Domain.Dialogporten.Infrastructure; internal sealed class UnitOfWork : IUnitOfWork { + private static readonly AsyncPolicyWrap ConcurrencyRetryPolicy; + private readonly DialogDbContext _dialogDbContext; private readonly ITransactionTime _transactionTime; private readonly IDomainContext _domainContext; private bool _auditableSideEffects = true; + private bool _enableConcurrencyCheck; public UnitOfWork(DialogDbContext dialogDbContext, ITransactionTime transactionTime, IDomainContext domainContext) { @@ -23,6 +32,44 @@ public UnitOfWork(DialogDbContext dialogDbContext, ITransactionTime transactionT _domainContext = domainContext ?? throw new ArgumentNullException(nameof(domainContext)); } + static UnitOfWork() + { + // Backoff strategy with jitter for retry policy, starting at ~5ms + const int medianFirstDelayInMs = 5; + // Total timeout for optimistic concurrency handling + const int timeoutInSeconds = 10; + + var timeoutPolicy = + Policy.TimeoutAsync(timeoutInSeconds, + TimeoutStrategy.Pessimistic, + (_, _, _) => throw new OptimisticConcurrencyTimeoutException()); + + // Fetch the db revision and retry + // https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations#resolving-concurrency-conflicts + var retryPolicy = Policy + .Handle() + .WaitAndRetryAsync( + sleepDurations: Backoff.DecorrelatedJitterBackoffV2( + medianFirstRetryDelay: TimeSpan.FromMilliseconds(medianFirstDelayInMs), + retryCount: int.MaxValue), + onRetryAsync: FetchCurrentRevision); + + ConcurrencyRetryPolicy = timeoutPolicy.WrapAsync(retryPolicy); + } + + public IUnitOfWork EnableConcurrencyCheck( + TEntity? entity, + Guid? revision) + where TEntity : class, IVersionableEntity + { + if (_dialogDbContext.TrySetOriginalRevision(entity, revision)) + { + _enableConcurrencyCheck = true; + } + + return this; + } + public IUnitOfWork WithoutAuditableSideEffects() { _auditableSideEffects = false; @@ -46,6 +93,14 @@ public async Task SaveChangesAsync(CancellationToken cancella await _dialogDbContext.ChangeTracker.HandleAuditableEntities(_transactionTime.Value, cancellationToken); } + if (!_enableConcurrencyCheck) + { + // Attempt to save changes without concurrency check + await ConcurrencyRetryPolicy.ExecuteAsync(ct => _dialogDbContext.SaveChangesAsync(ct), cancellationToken); + + return new Success(); + } + try { await _dialogDbContext.SaveChangesAsync(cancellationToken); @@ -57,4 +112,29 @@ public async Task SaveChangesAsync(CancellationToken cancella return new Success(); } + + private static async Task FetchCurrentRevision(Exception exception, TimeSpan _) + { + if (exception is not DbUpdateConcurrencyException concurrencyException) + { + return; + } + + foreach (var entry in concurrencyException.Entries) + { + if (entry.Entity is not IVersionableEntity) + { + continue; + } + + var dbValues = await entry.GetDatabaseValuesAsync(); + if (dbValues == null) + { + continue; + } + + var currentRevision = dbValues[nameof(IVersionableEntity.Revision)]!; + entry.Property(nameof(IVersionableEntity.Revision)).OriginalValue = currentRevision; + } + } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs index 9d7ca8a88..15f704af5 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs @@ -36,7 +36,7 @@ public override async Task HandleAsync(GetDialogQuery req, CancellationToken ct) await result.Match( dto => { - HttpContext.Response.Headers.ETag = dto.Revision.ToString(); + HttpContext.Response.Headers.ETag = dto.IfMatchDialogRevision.ToString(); return SendOkAsync(dto, ct); }, notFound => this.NotFoundAsync(notFound, ct), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/CreateDialogActivityEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/CreateDialogActivityEndpoint.cs index 59aee83c2..9b2b65e94 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/CreateDialogActivityEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogActivities/CreateDialogActivityEndpoint.cs @@ -60,7 +60,7 @@ public override async Task HandleAsync(CreateDialogActivityRequest req, Cancella updateDialogDto.Activities.Add(req); - var updateDialogCommand = new UpdateDialogCommand { Id = req.DialogId, Revision = req.Revision, Dto = updateDialogDto }; + var updateDialogCommand = new UpdateDialogCommand { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision, Dto = updateDialogDto }; var result = await _sender.Send(updateDialogCommand, ct); await result.Match( @@ -76,8 +76,8 @@ public sealed class CreateDialogActivityRequest : UpdateDialogDialogActivityDto { public Guid DialogId { get; set; } - [FromHeader(headerName: Constants.IfMatch, isRequired: false)] - public Guid? Revision { get; set; } + [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] + public Guid? IfMatchDialogRevision { get; set; } } public sealed class CreateDialogActivityEndpointSummary : Summary diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/CreateDialogElementEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/CreateDialogElementEndpoint.cs index e7cc54f87..d32e3e1b0 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/CreateDialogElementEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/CreateDialogElementEndpoint.cs @@ -56,7 +56,7 @@ public override async Task HandleAsync(CreateDialogElementRequest req, Cancellat updateDialogDto.Elements.Add(req); - var updateDialogCommand = new UpdateDialogCommand { Id = req.DialogId, Revision = req.Revision, Dto = updateDialogDto }; + var updateDialogCommand = new UpdateDialogCommand { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision, Dto = updateDialogDto }; var result = await _sender.Send(updateDialogCommand, ct); await result.Match( @@ -72,8 +72,8 @@ public sealed class CreateDialogElementRequest : UpdateDialogDialogElementDto { public Guid DialogId { get; set; } - [FromHeader(headerName: Constants.IfMatch, isRequired: false)] - public Guid? Revision { get; set; } + [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] + public Guid? IfMatchDialogRevision { get; set; } } public sealed class CreateDialogElementEndpointSummary : Summary diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/DeleteDialogElementEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/DeleteDialogElementEndpoint.cs index 7a21d8c0b..1746122e1 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/DeleteDialogElementEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/DeleteDialogElementEndpoint.cs @@ -64,7 +64,7 @@ public override async Task HandleAsync(DeleteDialogElementRequest req, Cancellat updateDialogDto.Elements.Remove(dialogElement); var updateDialogCommand = new UpdateDialogCommand - { Id = req.DialogId, Revision = req.Revision, Dto = updateDialogDto }; + { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision, Dto = updateDialogDto }; var result = await _sender.Send(updateDialogCommand, ct); await result.Match( @@ -82,7 +82,7 @@ public sealed class DeleteDialogElementRequest public Guid ElementId { get; set; } [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] - public Guid? Revision { get; set; } + public Guid? IfMatchDialogRevision { get; set; } } public sealed class DeleteDialogElementEndpointSummary : Summary diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/UpdateDialogElementEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/UpdateDialogElementEndpoint.cs index 08a74ec1c..9dac9fa91 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/UpdateDialogElementEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/DialogElements/UpdateDialogElementEndpoint.cs @@ -66,7 +66,7 @@ await this.NotFoundAsync( updateDialogDto.Elements.Add(updateDialogElementDto); var updateDialogCommand = new UpdateDialogCommand - { Id = req.DialogId, Revision = req.Revision, Dto = updateDialogDto }; + { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision, Dto = updateDialogDto }; var result = await _sender.Send(updateDialogCommand, ct); await result.Match( @@ -94,8 +94,8 @@ public sealed class UpdateDialogElementRequest public Guid ElementId { get; set; } - [FromHeader(headerName: Constants.IfMatch, isRequired: false)] - public Guid? Revision { get; set; } + [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] + public Guid? IfMatchDialogRevision { get; set; } public Uri? Type { get; set; } public string? AuthorizationAttribute { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/DeleteDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/DeleteDialogEndpoint.cs index d5ab0f9ce..f09f83e00 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/DeleteDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/DeleteDialogEndpoint.cs @@ -33,7 +33,7 @@ public override void Configure() public override async Task HandleAsync(DeleteDialogRequest req, CancellationToken ct) { - var command = new DeleteDialogCommand { Id = req.DialogId, Revision = req.Revision }; + var command = new DeleteDialogCommand { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision }; var result = await _sender.Send(command, ct); await result.Match( success => SendNoContentAsync(ct), @@ -47,7 +47,7 @@ public sealed class DeleteDialogRequest public Guid DialogId { get; set; } [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] - public Guid? Revision { get; set; } + public Guid? IfMatchDialogRevision { get; set; } } public sealed class DeleteDialogEndpointSummary : Summary diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/GetDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/GetDialogEndpoint.cs index 646ed2073..2e853c7d4 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/GetDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/GetDialogEndpoint.cs @@ -36,7 +36,7 @@ public override async Task HandleAsync(GetDialogQuery req, CancellationToken ct) await result.Match( dto => { - HttpContext.Response.Headers.ETag = dto.Revision.ToString(); + HttpContext.Response.Headers.ETag = dto.IfMatchDialogRevision.ToString(); return SendOkAsync(dto, ct); }, notFound => this.NotFoundAsync(notFound, ct)); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/PatchDialogsController.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/PatchDialogsController.cs index 8e58c7c7e..0a82eb163 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/PatchDialogsController.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/PatchDialogsController.cs @@ -77,7 +77,7 @@ public async Task Patch( return BadRequest(ModelState); } - var command = new UpdateDialogCommand { Id = dialogId, Revision = etag, Dto = updateDialogDto }; + var command = new UpdateDialogCommand { Id = dialogId, IfMatchDialogRevision = etag, Dto = updateDialogDto }; var result = await _sender.Send(command, ct); return result.Match( success => (IActionResult)NoContent(), diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/UpdateDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/UpdateDialogEndpoint.cs index ad34b963d..95c48c106 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/UpdateDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/ServiceOwner/Dialogs/UpdateDialogEndpoint.cs @@ -35,7 +35,7 @@ public override void Configure() public override async Task HandleAsync(UpdateDialogRequest req, CancellationToken ct) { - var command = new UpdateDialogCommand { Id = req.DialogId, Revision = req.Revision, Dto = req.Dto }; + var command = new UpdateDialogCommand { Id = req.DialogId, IfMatchDialogRevision = req.IfMatchDialogRevision, Dto = req.Dto }; var updateDialogResult = await _sender.Send(command, ct); await updateDialogResult.Match( success => SendNoContentAsync(ct), @@ -53,8 +53,8 @@ public sealed class UpdateDialogRequest [FromBody] public UpdateDialogDto Dto { get; set; } = null!; - [FromHeader(headerName: Constants.IfMatch, isRequired: false)] - public Guid? Revision { get; set; } + [FromHeader(headerName: Constants.IfMatch, isRequired: false, removeFromSchema: true)] + public Guid? IfMatchDialogRevision { get; set; } } public sealed class UpdateDialogEndpointSummary : Summary diff --git a/src/Digdir.Library.Entity.Abstractions/Features/EventPublisher/IEventPublisher.cs b/src/Digdir.Library.Entity.Abstractions/Features/EventPublisher/IEventPublisher.cs index f44915bca..6e8346d7e 100644 --- a/src/Digdir.Library.Entity.Abstractions/Features/EventPublisher/IEventPublisher.cs +++ b/src/Digdir.Library.Entity.Abstractions/Features/EventPublisher/IEventPublisher.cs @@ -6,7 +6,8 @@ namespace Digdir.Library.Entity.Abstractions.Features.EventPublisher; public interface IEventPublisher { /// - /// A collection of queued up for dispatching at the end of the next unit of work. + /// Get a collection of queued up for dispatching at the end of the next unit of work. + /// This also clears the collection on the EventPublisher. /// - public IReadOnlyCollection DomainEvents { get; } -} \ No newline at end of file + public IEnumerable PopDomainEvents(); +} diff --git a/src/Digdir.Library.Entity.EntityFrameworkCore/Features/EventPublisher/EventPublisherExtensions.cs b/src/Digdir.Library.Entity.EntityFrameworkCore/Features/EventPublisher/EventPublisherExtensions.cs index 1555a1265..defe4065e 100644 --- a/src/Digdir.Library.Entity.EntityFrameworkCore/Features/EventPublisher/EventPublisherExtensions.cs +++ b/src/Digdir.Library.Entity.EntityFrameworkCore/Features/EventPublisher/EventPublisherExtensions.cs @@ -1,15 +1,8 @@ -using Digdir.Library.Entity.Abstractions.Features.EventPublisher; using Microsoft.EntityFrameworkCore; namespace Digdir.Library.Entity.EntityFrameworkCore.Features.EventPublisher; internal static class EventPublisherExtensions { - internal static ModelBuilder AddEventPublisher(this ModelBuilder modelBuilder) - { - return modelBuilder.EntitiesOfType(builder => - { - builder.Ignore(nameof(IEventPublisher.DomainEvents)); - }); - } -} \ No newline at end of file + internal static ModelBuilder AddEventPublisher(this ModelBuilder modelBuilder) => modelBuilder; +} diff --git a/tests/k6/run.ps1 b/tests/k6/run.ps1 index 278437aae..498e20b10 100644 --- a/tests/k6/run.ps1 +++ b/tests/k6/run.ps1 @@ -49,8 +49,8 @@ if (-not (Test-Path $FilePath)) { } # Generate tests -& "$PSScriptRoot\scripts\generate_alltests.ps1" "$PSScriptRoot\tests\serviceowner\" > $null -& "$PSScriptRoot\scripts\generate_alltests.ps1" "$PSScriptRoot\tests\enduser\" > $null +& "$PSScriptRoot\scripts\generate_all_tests.ps1" "$PSScriptRoot\tests\serviceowner\" > $null +& "$PSScriptRoot\scripts\generate_all_tests.ps1" "$PSScriptRoot\tests\enduser\" > $null # Handle environment settings $insecureSkipTLS = $null diff --git a/tests/k6/run.sh b/tests/k6/run.sh old mode 100644 new mode 100755 index 538c7a7a6..50527890a --- a/tests/k6/run.sh +++ b/tests/k6/run.sh @@ -100,8 +100,8 @@ fi DIR="$(dirname "$0")" -"$DIR/scripts/generate_alltests.sh" "$DIR/tests/serviceowner/" >/dev/null -"$DIR/scripts/generate_alltests.sh" "$DIR/tests/enduser/" >/dev/null +"$DIR/scripts/generate_all_tests.sh" "$DIR/tests/serviceowner/" >/dev/null +"$DIR/scripts/generate_all_tests.sh" "$DIR/tests/enduser/" >/dev/null if [[ "$API_ENVIRONMENT" == "localdev" ]]; then # Handle self-signed certs when using docker compose diff --git a/tests/k6/scripts/generate_alltests.ps1 b/tests/k6/scripts/generate_all_tests.ps1 similarity index 100% rename from tests/k6/scripts/generate_alltests.ps1 rename to tests/k6/scripts/generate_all_tests.ps1 diff --git a/tests/k6/scripts/generate_alltests.sh b/tests/k6/scripts/generate_all_tests.sh old mode 100644 new mode 100755 similarity index 100% rename from tests/k6/scripts/generate_alltests.sh rename to tests/k6/scripts/generate_all_tests.sh diff --git a/tests/k6/tests/serviceowner/concurrency.js b/tests/k6/tests/serviceowner/concurrency.js index bb3a16a92..e8fc9ff7a 100644 --- a/tests/k6/tests/serviceowner/concurrency.js +++ b/tests/k6/tests/serviceowner/concurrency.js @@ -33,4 +33,4 @@ export default function () { }); }); -} \ No newline at end of file +}