diff --git a/.editorconfig b/.editorconfig index 59e3b23..406cd49 100644 --- a/.editorconfig +++ b/.editorconfig @@ -346,10 +346,15 @@ dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity # Private fields must be camelCase # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md -dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private -dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = warning +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + # Local variables must be camelCase # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md diff --git a/src/BuildingBlocks/EFCore/AppDbContextBase.cs b/src/BuildingBlocks/EFCore/AppDbContextBase.cs index 55dac6e..a8fe3a1 100644 --- a/src/BuildingBlocks/EFCore/AppDbContextBase.cs +++ b/src/BuildingBlocks/EFCore/AppDbContextBase.cs @@ -1,24 +1,28 @@ using System.Collections.Immutable; using BuildingBlocks.Core.Event; using BuildingBlocks.Core.Model; -using BuildingBlocks.Web; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; namespace BuildingBlocks.EFCore; using System.Data; +using System.Net; +using System.Security.Claims; +using global::Polly; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; public abstract class AppDbContextBase : DbContext, IDbContext { - private readonly ICurrentUserProvider _currentUserProvider; - + private readonly IHttpContextAccessor _httpContextAccessor; private IDbContextTransaction _currentTransaction; - protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider currentUserProvider = null) : + protected AppDbContextBase(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) : base(options) { - _currentUserProvider = currentUserProvider; + _httpContextAccessor = httpContextAccessor; } protected override void OnModelCreating(ModelBuilder builder) @@ -68,44 +72,49 @@ public abstract class AppDbContextBase : DbContext, IDbContext } } - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { OnBeforeSaving(); try { - return base.SaveChangesAsync(cancellationToken); + return await base.SaveChangesAsync(cancellationToken); } //ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api#resolving-concurrency-conflicts catch (DbUpdateConcurrencyException ex) { - foreach (var entry in ex.Entries) + var logger = _httpContextAccessor?.HttpContext?.RequestServices + .GetRequiredService>(); + + var entry = ex.Entries.SingleOrDefault(); + + if (entry == null) { - var proposedValues = entry.CurrentValues; - var databaseValues = entry.GetDatabaseValues(); - - if (databaseValues != null) - { - // update the original values with the database values - entry.OriginalValues.SetValues(databaseValues); - - // check for conflicts - if (!proposedValues.Equals(databaseValues)) - { - if (entry.Entity.GetType() == typeof(IAggregate)) - { - // merge concurrency conflict for IAggregate - } - else - { - throw new NotSupportedException( - "Don't know how to handle concurrency conflicts for " - + entry.Metadata.Name); - } - } - } + return 0; } - return base.SaveChangesAsync(cancellationToken); + var currentValue = entry.CurrentValues; + var databaseValue = await entry.GetDatabaseValuesAsync(cancellationToken); + + logger?.LogInformation("The entity being updated is already use by another Thread!" + + " database value is: {DatabaseValue} and current value is: {CurrentValue}", + databaseValue, currentValue); + + var policy = Policy.Handle() + .WaitAndRetryAsync(retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(1), + onRetry: (exception, timeSpan, retryCount, context) => + { + if (exception != null) + { + logger?.LogError(exception, + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", + HttpStatusCode.Conflict, + timeSpan, + retryCount); + } + }); + + return await policy.ExecuteAsync(async () => await base.SaveChangesAsync(cancellationToken)); } } @@ -133,7 +142,7 @@ public abstract class AppDbContextBase : DbContext, IDbContext foreach (var entry in ChangeTracker.Entries()) { var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate)); - var userId = _currentUserProvider?.GetCurrentUserId(); + var userId = GetCurrentUserId(); if (isAuditable) { @@ -161,4 +170,13 @@ public abstract class AppDbContextBase : DbContext, IDbContext } } } + + private long? GetCurrentUserId() + { + var nameIdentifier = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); + + long.TryParse(nameIdentifier, out var userId); + + return userId; + } } diff --git a/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs b/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs index 50f3256..1464b97 100644 --- a/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs +++ b/src/BuildingBlocks/Exception/GrpcExceptionInterceptor.cs @@ -17,7 +17,7 @@ public class GrpcExceptionInterceptor : Interceptor } catch (System.Exception exception) { - throw new RpcException(new Status(StatusCode.Cancelled, exception.Message)); + throw new RpcException(new Status(StatusCode.Internal, exception.Message)); } } } diff --git a/src/BuildingBlocks/MassTransit/Extensions.cs b/src/BuildingBlocks/MassTransit/Extensions.cs index a4fa73f..a0c89d2 100644 --- a/src/BuildingBlocks/MassTransit/Extensions.cs +++ b/src/BuildingBlocks/MassTransit/Extensions.cs @@ -9,6 +9,8 @@ using Microsoft.Extensions.Hosting; namespace BuildingBlocks.MassTransit; +using Exception; + public static class Extensions { private static bool? _isRunningInContainer; @@ -80,6 +82,10 @@ public static class Extensions foreach (var consumer in consumers) { + //ref: https://masstransit-project.com/usage/exceptions.html#retry + //ref: https://markgossa.com/2022/06/masstransit-exponential-back-off.html + configurator.UseMessageRetry(r => AddRetryConfiguration(r)); + configurator.ConfigureEndpoints(context, x => x.Exclude(consumer)); var methodInfo = typeof(DependencyInjectionReceiveEndpointExtensions) .GetMethods() @@ -95,4 +101,16 @@ public static class Extensions } }); } + + private static IRetryConfigurator AddRetryConfiguration(IRetryConfigurator retryConfigurator) + { + retryConfigurator.Exponential( + 3, + TimeSpan.FromMilliseconds(200), + TimeSpan.FromMinutes(120), + TimeSpan.FromMilliseconds(200)) + .Ignore(); // don't retry if we have invalid data and message goes to _error queue masstransit + + return retryConfigurator; + } } diff --git a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs index 2db8d66..295868b 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/Data/PersistMessageDbContext.cs @@ -4,10 +4,12 @@ using Microsoft.EntityFrameworkCore; namespace BuildingBlocks.PersistMessageProcessor.Data; +using Microsoft.AspNetCore.Http; + public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext { - public PersistMessageDbContext(DbContextOptions options) - : base(options) + public PersistMessageDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) + : base(options, httpContextAccessor) { } diff --git a/src/BuildingBlocks/PersistMessageProcessor/PersistMessageBackgroundService.cs b/src/BuildingBlocks/PersistMessageProcessor/PersistMessageBackgroundService.cs index 09527f9..36deb53 100644 --- a/src/BuildingBlocks/PersistMessageProcessor/PersistMessageBackgroundService.cs +++ b/src/BuildingBlocks/PersistMessageProcessor/PersistMessageBackgroundService.cs @@ -43,25 +43,17 @@ public class PersistMessageBackgroundService : BackgroundService { while (!stoppingToken.IsCancellationRequested) { - try + await using (var scope = _serviceProvider.CreateAsyncScope()) { - await using (var scope = _serviceProvider.CreateAsyncScope()) - { - var service = scope.ServiceProvider.GetRequiredService(); - await service.ProcessAllAsync(stoppingToken); - } - - var delay = _options.Interval is { } - ? TimeSpan.FromSeconds((int)_options.Interval) - : TimeSpan.FromSeconds(30); - - await Task.Delay(delay, stoppingToken); - } - catch (System.Exception e) - { - Console.WriteLine(e); - throw; + var service = scope.ServiceProvider.GetRequiredService(); + await service.ProcessAllAsync(stoppingToken); } + + var delay = _options.Interval is { } + ? TimeSpan.FromSeconds((int)_options.Interval) + : TimeSpan.FromSeconds(30); + + await Task.Delay(delay, stoppingToken); } } } diff --git a/src/BuildingBlocks/Polly/CircuitBreakerOptions.cs b/src/BuildingBlocks/Polly/CircuitBreakerOptions.cs new file mode 100644 index 0000000..ae54e41 --- /dev/null +++ b/src/BuildingBlocks/Polly/CircuitBreakerOptions.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.Polly; + +public class CircuitBreakerOptions +{ + public int RetryCount { get; set; } + public int BreakDuration { get; set; } +} diff --git a/src/BuildingBlocks/Polly/GrpcCircuitBreaker.cs b/src/BuildingBlocks/Polly/GrpcCircuitBreaker.cs new file mode 100644 index 0000000..dd2e375 --- /dev/null +++ b/src/BuildingBlocks/Polly/GrpcCircuitBreaker.cs @@ -0,0 +1,83 @@ +namespace BuildingBlocks.Polly; + +using System.Net; +using Ardalis.GuardClauses; +using BuildingBlocks.Web; +using global::Polly; +using Grpc.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +public static class GrpcCircuitBreaker +{ + //ref: https://anthonygiretti.com/2020/03/31/grpc-asp-net-core-3-1-resiliency-with-polly/ + public static IHttpClientBuilder AddGrpcCircuitBreakerPolicyHandler(this IHttpClientBuilder httpClientBuilder) + { + return httpClientBuilder.AddPolicyHandler((sp, _) => + { + var options = sp.GetRequiredService().GetOptions(nameof(PolicyOptions)); + + Guard.Against.Null(options, nameof(options)); + + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PollyGrpcCircuitBreakerPoliciesLogger"); + + // gRPC status + var gRpcErrors = new StatusCode[] + { + StatusCode.DeadlineExceeded, StatusCode.Internal, StatusCode.NotFound, StatusCode.Cancelled, + StatusCode.ResourceExhausted, StatusCode.Unavailable, StatusCode.Unknown + }; + + // Http errors + var serverErrors = new HttpStatusCode[] + { + HttpStatusCode.BadGateway, HttpStatusCode.GatewayTimeout, HttpStatusCode.ServiceUnavailable, + HttpStatusCode.InternalServerError, HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout + }; + + return Policy.HandleResult(r => + { + var grpcStatus = StatusManager.GetStatusCode(r); + var httpStatusCode = r.StatusCode; + + return (grpcStatus == null && serverErrors.Contains(httpStatusCode)) || // if the server send an error before gRPC pipeline + (httpStatusCode == HttpStatusCode.OK && gRpcErrors.Contains(grpcStatus.Value)); // if gRPC pipeline handled the request (gRPC always answers OK) + }) + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: options.CircuitBreaker.RetryCount, + durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreaker.BreakDuration), + onBreak: (response, breakDuration) => + { + if (response?.Exception != null) + { + logger.LogError(response.Exception, + "Service shutdown during {BreakDuration} after {RetryCount} failed retries", + breakDuration, + options.CircuitBreaker.RetryCount); + } + }, + onReset: () => + { + logger.LogInformation("Service restarted"); + }); + }); + } + + private static class StatusManager + { + public static StatusCode? GetStatusCode(HttpResponseMessage response) + { + var headers = response.Headers; + + if (!headers.Contains("grpc-status") && response.StatusCode == HttpStatusCode.OK) + return StatusCode.OK; + + if (headers.Contains("grpc-status")) + return (StatusCode)int.Parse(headers.GetValues("grpc-status").First()); + + return null; + } + } +} diff --git a/src/BuildingBlocks/Polly/GrpcRetry.cs b/src/BuildingBlocks/Polly/GrpcRetry.cs new file mode 100644 index 0000000..a273830 --- /dev/null +++ b/src/BuildingBlocks/Polly/GrpcRetry.cs @@ -0,0 +1,79 @@ +namespace BuildingBlocks.Polly; + +using System.Net; +using Ardalis.GuardClauses; +using BuildingBlocks.Web; +using global::Polly; +using Grpc.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +public static class GrpcRetry +{ + //ref: https://anthonygiretti.com/2020/03/31/grpc-asp-net-core-3-1-resiliency-with-polly/ + public static IHttpClientBuilder AddGrpcRetryPolicyHandler(this IHttpClientBuilder httpClientBuilder) + { + return httpClientBuilder.AddPolicyHandler((sp, _) => + { + var options = sp.GetRequiredService().GetOptions(nameof(PolicyOptions)); + + Guard.Against.Null(options, nameof(options)); + + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PollyGrpcRetryPoliciesLogger"); + + // gRPC status + var gRpcErrors = new StatusCode[] + { + StatusCode.DeadlineExceeded, StatusCode.Internal, StatusCode.NotFound, StatusCode.Cancelled, + StatusCode.ResourceExhausted, StatusCode.Unavailable, StatusCode.Unknown + }; + + // Http errors + var serverErrors = new HttpStatusCode[] + { + HttpStatusCode.BadGateway, HttpStatusCode.GatewayTimeout, HttpStatusCode.ServiceUnavailable, + HttpStatusCode.InternalServerError, HttpStatusCode.TooManyRequests, HttpStatusCode.RequestTimeout + }; + + return Policy.HandleResult(r => + { + var grpcStatus = StatusManager.GetStatusCode(r); + var httpStatusCode = r.StatusCode; + + return (grpcStatus == null && serverErrors.Contains(httpStatusCode)) || // if the server send an error before gRPC pipeline + (httpStatusCode == HttpStatusCode.OK && gRpcErrors.Contains(grpcStatus.Value)); // if gRPC pipeline handled the request (gRPC always answers OK) + }) + .WaitAndRetryAsync(retryCount: options.Retry.RetryCount, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(options.Retry.SleepDuration), + onRetry: (response, timeSpan, retryCount, context) => + { + if (response?.Exception != null) + { + logger.LogError(response.Exception, + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", + response.Result.StatusCode, + timeSpan, + retryCount); + } + }); + }); + } + + private static class StatusManager + { + public static StatusCode? GetStatusCode(HttpResponseMessage response) + { + var headers = response.Headers; + + if (!headers.Contains("grpc-status") && response.StatusCode == HttpStatusCode.OK) + return StatusCode.OK; + + if (headers.Contains("grpc-status")) + return (StatusCode)int.Parse(headers.GetValues("grpc-status").First()); + + return null; + } + } +} diff --git a/src/BuildingBlocks/Polly/HttpClientCircuitBreaker.cs b/src/BuildingBlocks/Polly/HttpClientCircuitBreaker.cs new file mode 100644 index 0000000..699a22a --- /dev/null +++ b/src/BuildingBlocks/Polly/HttpClientCircuitBreaker.cs @@ -0,0 +1,48 @@ +namespace BuildingBlocks.Polly; + +using System.Net; +using Ardalis.GuardClauses; +using BuildingBlocks.Web; +using global::Polly; +using global::Polly.Extensions.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Exception = System.Exception; + +public static class HttpClientCircuitBreaker +{ + // ref: https://anthonygiretti.com/2019/03/26/best-practices-with-httpclient-and-retry-policies-with-polly-in-net-core-2-part-2/ + public static IHttpClientBuilder AddHttpClientCircuitBreakerPolicyHandler(this IHttpClientBuilder httpClientBuilder) + { + return httpClientBuilder.AddPolicyHandler((sp, _) => + { + var options = sp.GetRequiredService().GetOptions(nameof(PolicyOptions)); + + Guard.Against.Null(options, nameof(options)); + + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PollyHttpClientCircuitBreakerPoliciesLogger"); + + return HttpPolicyExtensions.HandleTransientHttpError() + .OrResult(msg => msg.StatusCode == HttpStatusCode.BadRequest) + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: options.CircuitBreaker.RetryCount, + durationOfBreak: TimeSpan.FromSeconds(options.CircuitBreaker.BreakDuration), + onBreak: (response, breakDuration) => + { + if (response?.Exception != null) + { + logger.LogError(response.Exception, + "Service shutdown during {BreakDuration} after {RetryCount} failed retries", + breakDuration, + options.CircuitBreaker.RetryCount); + } + }, + onReset: () => + { + logger.LogInformation("Service restarted"); + }); + }); + } +} diff --git a/src/BuildingBlocks/Polly/HttpClientRetry.cs b/src/BuildingBlocks/Polly/HttpClientRetry.cs new file mode 100644 index 0000000..cfcd1eb --- /dev/null +++ b/src/BuildingBlocks/Polly/HttpClientRetry.cs @@ -0,0 +1,44 @@ +namespace BuildingBlocks.Polly; + +using System.Net; +using Ardalis.GuardClauses; +using BuildingBlocks.Web; +using global::Polly; +using global::Polly.Extensions.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +public static class HttpClientRetry +{ + // ref: https://anthonygiretti.com/2019/03/26/best-practices-with-httpclient-and-retry-policies-with-polly-in-net-core-2-part-2/ + public static IHttpClientBuilder AddHttpClientRetryPolicyHandler(this IHttpClientBuilder httpClientBuilder) + { + return httpClientBuilder.AddPolicyHandler((sp, _) => + { + var options = sp.GetRequiredService().GetOptions(nameof(PolicyOptions)); + + Guard.Against.Null(options, nameof(options)); + + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("PollyHttpClientRetryPoliciesLogger"); + + return HttpPolicyExtensions.HandleTransientHttpError() + .OrResult(msg => msg.StatusCode == HttpStatusCode.BadRequest) + .OrResult(msg => msg.StatusCode == HttpStatusCode.InternalServerError) + .WaitAndRetryAsync(retryCount: options.Retry.RetryCount, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(options.Retry.SleepDuration), + onRetry: (response, timeSpan, retryCount, context) => + { + if (response?.Exception != null) + { + logger.LogError(response.Exception, + "Request failed with {StatusCode}. Waiting {TimeSpan} before next retry. Retry attempt {RetryCount}.", + response.Result.StatusCode, + timeSpan, + retryCount); + } + }); + }); + } +} diff --git a/src/BuildingBlocks/Polly/PolicyOptions.cs b/src/BuildingBlocks/Polly/PolicyOptions.cs new file mode 100644 index 0000000..1b18530 --- /dev/null +++ b/src/BuildingBlocks/Polly/PolicyOptions.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.Polly; + +public class PolicyOptions +{ + public RetryOptions Retry { get; set; } + public CircuitBreakerOptions CircuitBreaker { get; set; } +} diff --git a/src/BuildingBlocks/Polly/RetryOptions.cs b/src/BuildingBlocks/Polly/RetryOptions.cs new file mode 100644 index 0000000..51c5f53 --- /dev/null +++ b/src/BuildingBlocks/Polly/RetryOptions.cs @@ -0,0 +1,7 @@ +namespace BuildingBlocks.Polly; + +public class RetryOptions +{ + public int RetryCount { get; set; } + public int SleepDuration { get; set; } +} diff --git a/src/BuildingBlocks/TestBase/TestBase.cs b/src/BuildingBlocks/TestBase/TestBase.cs index bb72ea9..23429cc 100644 --- a/src/BuildingBlocks/TestBase/TestBase.cs +++ b/src/BuildingBlocks/TestBase/TestBase.cs @@ -408,6 +408,14 @@ public class TestReadFixture : TestFixture { return ExecuteScopeAsync(sp => action(sp.GetRequiredService())); } + + public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class + { + await ExecuteReadContextAsync(async db => + { + await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); + }); + } } public class TestFixture : TestWriteFixture @@ -424,6 +432,14 @@ public class TestFixture : TestWriteFixture action(sp.GetRequiredService())); } + + public async Task InsertMongoDbContextAsync(string collectionName, params T[] entities) where T : class + { + await ExecuteReadContextAsync(async db => + { + await db.GetCollection(collectionName).InsertManyAsync(entities.ToList()); + }); + } } public class TestFixtureCore : IAsyncLifetime diff --git a/src/Services/Booking/src/Booking.Api/appsettings.json b/src/Services/Booking/src/Booking.Api/appsettings.json index cb80b35..9796ad1 100644 --- a/src/Services/Booking/src/Booking.Api/appsettings.json +++ b/src/Services/Booking/src/Booking.Api/appsettings.json @@ -36,6 +36,16 @@ "FlightAddress": "https://localhost:5003", "PassengerAddress": "https://localhost:5012" }, + "RetryOptions": { + "Retry": { + "RetryCount": 3, + "SleepDuration": 1 + }, + "CircuitBreaker": { + "RetryCount": 5, + "BreakDuration" : 30 + } + }, "EventStore": { "ConnectionString": "esdb://localhost:2113?tls=false" }, diff --git a/src/Services/Booking/src/Booking/Extensions/Infrastructure/GrpcClientExtensions.cs b/src/Services/Booking/src/Booking/Extensions/Infrastructure/GrpcClientExtensions.cs index f48b0ad..d2fc821 100644 --- a/src/Services/Booking/src/Booking/Extensions/Infrastructure/GrpcClientExtensions.cs +++ b/src/Services/Booking/src/Booking/Extensions/Infrastructure/GrpcClientExtensions.cs @@ -6,6 +6,8 @@ using Passenger; namespace Booking.Extensions.Infrastructure; +using BuildingBlocks.Polly; + public static class GrpcClientExtensions { public static IServiceCollection AddGrpcClients(this IServiceCollection services) @@ -15,12 +17,16 @@ public static class GrpcClientExtensions services.AddGrpcClient(o => { o.Address = new Uri(grpcOptions.FlightAddress); - }); + }) + .AddGrpcRetryPolicyHandler() + .AddGrpcCircuitBreakerPolicyHandler(); services.AddGrpcClient(o => { o.Address = new Uri(grpcOptions.PassengerAddress); - }); + }) + .AddGrpcRetryPolicyHandler() + .AddGrpcCircuitBreakerPolicyHandler();; return services; } diff --git a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs index ee7a765..00cdc5f 100644 --- a/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs +++ b/src/Services/Booking/src/Booking/Extensions/Infrastructure/InfrastructureExtensions.cs @@ -78,7 +78,7 @@ public static class InfrastructureExtensions SnowFlakIdGenerator.Configure(3); -// ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce + // ref: https://github.com/oskardudycz/EventSourcing.NetCore/tree/main/Sample/EventStoreDB/ECommerce builder.Services.AddEventStore(configuration, typeof(BookingRoot).Assembly) .AddEventStoreDBSubscriptionToAll(); diff --git a/src/Services/Booking/src/Booking/Extensions/Infrastructure/ProblemDetailsExtensions.cs b/src/Services/Booking/src/Booking/Extensions/Infrastructure/ProblemDetailsExtensions.cs index 6b59b3c..9e3aa31 100644 --- a/src/Services/Booking/src/Booking/Extensions/Infrastructure/ProblemDetailsExtensions.cs +++ b/src/Services/Booking/src/Booking/Extensions/Infrastructure/ProblemDetailsExtensions.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Hosting; namespace Booking.Extensions.Infrastructure; +using Microsoft.EntityFrameworkCore; + public static class ProblemDetailsExtensions { public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) @@ -74,6 +76,14 @@ public static class ProblemDetailsExtensions Type = "https://somedomain/grpc-error" }); + x.Map(ex => new ProblemDetailsWithCode + { + Title = ex.GetType().Name, + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/db-update-concurrency-error" + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); x.MapStatusCode = context => diff --git a/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs index 6047782..b9a7a6c 100644 --- a/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs +++ b/src/Services/Flight/src/Flight/Data/DesignTimeDbContextFactory.cs @@ -11,7 +11,7 @@ namespace Flight.Data builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true") .UseSnakeCaseNamingConvention(); - return new FlightDbContext(builder.Options, null); + return new FlightDbContext(builder.Options); } } } diff --git a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs index 60e6e13..a13cabe 100644 --- a/src/Services/Flight/src/Flight/Data/FlightDbContext.cs +++ b/src/Services/Flight/src/Flight/Data/FlightDbContext.cs @@ -1,5 +1,4 @@ using BuildingBlocks.EFCore; -using BuildingBlocks.Web; using Flight.Aircrafts.Models; using Flight.Airports.Models; using Flight.Seats.Models; @@ -7,10 +6,12 @@ using Microsoft.EntityFrameworkCore; namespace Flight.Data; +using Microsoft.AspNetCore.Http; + public sealed class FlightDbContext : AppDbContextBase { - public FlightDbContext(DbContextOptions options, ICurrentUserProvider currentUserProvider) : base( - options, currentUserProvider) + public FlightDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) : base( + options, httpContextAccessor) { } diff --git a/src/Services/Flight/src/Flight/Extensions/Infrastructure/ProblemDetailsExtensions.cs b/src/Services/Flight/src/Flight/Extensions/Infrastructure/ProblemDetailsExtensions.cs index a74654e..006dd8b 100644 --- a/src/Services/Flight/src/Flight/Extensions/Infrastructure/ProblemDetailsExtensions.cs +++ b/src/Services/Flight/src/Flight/Extensions/Infrastructure/ProblemDetailsExtensions.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting; namespace Flight.Extensions.Infrastructure; +using Microsoft.EntityFrameworkCore; + public static class ProblemDetailsExtensions { public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) @@ -65,6 +67,14 @@ public static class ProblemDetailsExtensions Type = "https://somedomain/application-error" }); + x.Map(ex => new ProblemDetailsWithCode + { + Title = ex.GetType().Name, + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/db-update-concurrency-error" + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); x.MapStatusCode = context => diff --git a/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs b/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs index ee758cd..c068f6f 100644 --- a/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs +++ b/src/Services/Flight/tests/UnitTest/Common/DbContextFactory.cs @@ -18,7 +18,7 @@ namespace Unit.Test.Common var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options; - var context = new FlightDbContext(options, currentUserProvider: null); + var context = new FlightDbContext(options); // Seed our data FlightDataSeeder(context); diff --git a/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs b/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs index 2fefae6..40f5138 100644 --- a/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs +++ b/src/Services/Identity/src/Identity/Data/DesignTimeDbContextFactory.cs @@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory options, IHttpContextAccessor httpContextAccessor) : + public IdentityContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) : base(options) { } diff --git a/src/Services/Identity/src/Identity/Extensions/Infrastructure/ProblemDetailsExtensions.cs b/src/Services/Identity/src/Identity/Extensions/Infrastructure/ProblemDetailsExtensions.cs index c2a4c2c..829252b 100644 --- a/src/Services/Identity/src/Identity/Extensions/Infrastructure/ProblemDetailsExtensions.cs +++ b/src/Services/Identity/src/Identity/Extensions/Infrastructure/ProblemDetailsExtensions.cs @@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting; namespace Identity.Extensions.Infrastructure; +using Microsoft.EntityFrameworkCore; + public static class ProblemDetailsExtensions { public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) @@ -65,6 +67,14 @@ public static class ProblemDetailsExtensions Type = "https://somedomain/application-error" }); + x.Map(ex => new ProblemDetailsWithCode + { + Title = ex.GetType().Name, + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/db-update-concurrency-error" + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); x.MapStatusCode = context => diff --git a/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs index 87860fb..d56404d 100644 --- a/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs +++ b/src/Services/Passenger/src/Passenger/Data/DesignTimeDbContextFactory.cs @@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory: IDesignTimeDbContextFactory options, ICurrentUserProvider currentUserProvider) : base(options, currentUserProvider) + public PassengerDbContext(DbContextOptions options, + IHttpContextAccessor httpContextAccessor = default) : + base(options, httpContextAccessor) { } diff --git a/src/Services/Passenger/src/Passenger/Extensions/Infrastructure/ProblemDetailsExtensions.cs b/src/Services/Passenger/src/Passenger/Extensions/Infrastructure/ProblemDetailsExtensions.cs index 3eb728b..c785cee 100644 --- a/src/Services/Passenger/src/Passenger/Extensions/Infrastructure/ProblemDetailsExtensions.cs +++ b/src/Services/Passenger/src/Passenger/Extensions/Infrastructure/ProblemDetailsExtensions.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.Hosting; namespace Passenger.Extensions.Infrastructure; +using Microsoft.EntityFrameworkCore; + public static class ProblemDetailsExtensions { public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) @@ -64,6 +66,14 @@ public static class ProblemDetailsExtensions Type = "https://somedomain/application-error" }); + x.Map(ex => new ProblemDetailsWithCode + { + Title = ex.GetType().Name, + Status = StatusCodes.Status409Conflict, + Detail = ex.Message, + Type = "https://somedomain/db-update-concurrency-error" + }); + x.MapToStatusCode(StatusCodes.Status400BadRequest); x.MapStatusCode = context =>