mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-18 09:52:11 +08:00
commit
c09f854b28
@ -346,10 +346,15 @@ dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity
|
|||||||
|
|
||||||
# Private fields must be camelCase
|
# Private fields must be camelCase
|
||||||
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md
|
# 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.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
|
# Local variables must be camelCase
|
||||||
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
|
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using BuildingBlocks.Core.Event;
|
using BuildingBlocks.Core.Event;
|
||||||
using BuildingBlocks.Core.Model;
|
using BuildingBlocks.Core.Model;
|
||||||
using BuildingBlocks.Web;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
|
||||||
namespace BuildingBlocks.EFCore;
|
namespace BuildingBlocks.EFCore;
|
||||||
|
|
||||||
using System.Data;
|
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
|
public abstract class AppDbContextBase : DbContext, IDbContext
|
||||||
{
|
{
|
||||||
private readonly ICurrentUserProvider _currentUserProvider;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
private IDbContextTransaction _currentTransaction;
|
private IDbContextTransaction _currentTransaction;
|
||||||
|
|
||||||
protected AppDbContextBase(DbContextOptions options, ICurrentUserProvider currentUserProvider = null) :
|
protected AppDbContextBase(DbContextOptions options, IHttpContextAccessor httpContextAccessor = default) :
|
||||||
base(options)
|
base(options)
|
||||||
{
|
{
|
||||||
_currentUserProvider = currentUserProvider;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
@ -68,44 +72,49 @@ public abstract class AppDbContextBase : DbContext, IDbContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
OnBeforeSaving();
|
OnBeforeSaving();
|
||||||
try
|
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
|
//ref: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=fluent-api#resolving-concurrency-conflicts
|
||||||
catch (DbUpdateConcurrencyException ex)
|
catch (DbUpdateConcurrencyException ex)
|
||||||
{
|
{
|
||||||
foreach (var entry in ex.Entries)
|
var logger = _httpContextAccessor?.HttpContext?.RequestServices
|
||||||
|
.GetRequiredService<ILogger<AppDbContextBase>>();
|
||||||
|
|
||||||
|
var entry = ex.Entries.SingleOrDefault();
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
{
|
{
|
||||||
var proposedValues = entry.CurrentValues;
|
return 0;
|
||||||
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 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<DbUpdateConcurrencyException>()
|
||||||
|
.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<IAggregate>())
|
foreach (var entry in ChangeTracker.Entries<IAggregate>())
|
||||||
{
|
{
|
||||||
var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
|
var isAuditable = entry.Entity.GetType().IsAssignableTo(typeof(IAggregate));
|
||||||
var userId = _currentUserProvider?.GetCurrentUserId();
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
if (isAuditable)
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public class GrpcExceptionInterceptor : Interceptor
|
|||||||
}
|
}
|
||||||
catch (System.Exception exception)
|
catch (System.Exception exception)
|
||||||
{
|
{
|
||||||
throw new RpcException(new Status(StatusCode.Cancelled, exception.Message));
|
throw new RpcException(new Status(StatusCode.Internal, exception.Message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
namespace BuildingBlocks.MassTransit;
|
namespace BuildingBlocks.MassTransit;
|
||||||
|
|
||||||
|
using Exception;
|
||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
private static bool? _isRunningInContainer;
|
private static bool? _isRunningInContainer;
|
||||||
@ -80,6 +82,10 @@ public static class Extensions
|
|||||||
|
|
||||||
foreach (var consumer in consumers)
|
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));
|
configurator.ConfigureEndpoints(context, x => x.Exclude(consumer));
|
||||||
var methodInfo = typeof(DependencyInjectionReceiveEndpointExtensions)
|
var methodInfo = typeof(DependencyInjectionReceiveEndpointExtensions)
|
||||||
.GetMethods()
|
.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<ValidationException>(); // don't retry if we have invalid data and message goes to _error queue masstransit
|
||||||
|
|
||||||
|
return retryConfigurator;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace BuildingBlocks.PersistMessageProcessor.Data;
|
namespace BuildingBlocks.PersistMessageProcessor.Data;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext
|
public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext
|
||||||
{
|
{
|
||||||
public PersistMessageDbContext(DbContextOptions<PersistMessageDbContext> options)
|
public PersistMessageDbContext(DbContextOptions<PersistMessageDbContext> options, IHttpContextAccessor httpContextAccessor = default)
|
||||||
: base(options)
|
: base(options, httpContextAccessor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,25 +43,17 @@ public class PersistMessageBackgroundService : BackgroundService
|
|||||||
{
|
{
|
||||||
while (!stoppingToken.IsCancellationRequested)
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
await using (var scope = _serviceProvider.CreateAsyncScope())
|
||||||
{
|
{
|
||||||
await using (var scope = _serviceProvider.CreateAsyncScope())
|
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
|
||||||
{
|
await service.ProcessAllAsync(stoppingToken);
|
||||||
var service = scope.ServiceProvider.GetRequiredService<IPersistMessageProcessor>();
|
|
||||||
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 delay = _options.Interval is { }
|
||||||
|
? TimeSpan.FromSeconds((int)_options.Interval)
|
||||||
|
: TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
await Task.Delay(delay, stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/BuildingBlocks/Polly/CircuitBreakerOptions.cs
Normal file
7
src/BuildingBlocks/Polly/CircuitBreakerOptions.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace BuildingBlocks.Polly;
|
||||||
|
|
||||||
|
public class CircuitBreakerOptions
|
||||||
|
{
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
public int BreakDuration { get; set; }
|
||||||
|
}
|
||||||
83
src/BuildingBlocks/Polly/GrpcCircuitBreaker.cs
Normal file
83
src/BuildingBlocks/Polly/GrpcCircuitBreaker.cs
Normal file
@ -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<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
|
||||||
|
|
||||||
|
Guard.Against.Null(options, nameof(options));
|
||||||
|
|
||||||
|
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||||
|
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<HttpResponseMessage>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/BuildingBlocks/Polly/GrpcRetry.cs
Normal file
79
src/BuildingBlocks/Polly/GrpcRetry.cs
Normal file
@ -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<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
|
||||||
|
|
||||||
|
Guard.Against.Null(options, nameof(options));
|
||||||
|
|
||||||
|
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||||
|
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<HttpResponseMessage>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/BuildingBlocks/Polly/HttpClientCircuitBreaker.cs
Normal file
48
src/BuildingBlocks/Polly/HttpClientCircuitBreaker.cs
Normal file
@ -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<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
|
||||||
|
|
||||||
|
Guard.Against.Null(options, nameof(options));
|
||||||
|
|
||||||
|
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/BuildingBlocks/Polly/HttpClientRetry.cs
Normal file
44
src/BuildingBlocks/Polly/HttpClientRetry.cs
Normal file
@ -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<IConfiguration>().GetOptions<PolicyOptions>(nameof(PolicyOptions));
|
||||||
|
|
||||||
|
Guard.Against.Null(options, nameof(options));
|
||||||
|
|
||||||
|
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/BuildingBlocks/Polly/PolicyOptions.cs
Normal file
7
src/BuildingBlocks/Polly/PolicyOptions.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace BuildingBlocks.Polly;
|
||||||
|
|
||||||
|
public class PolicyOptions
|
||||||
|
{
|
||||||
|
public RetryOptions Retry { get; set; }
|
||||||
|
public CircuitBreakerOptions CircuitBreaker { get; set; }
|
||||||
|
}
|
||||||
7
src/BuildingBlocks/Polly/RetryOptions.cs
Normal file
7
src/BuildingBlocks/Polly/RetryOptions.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace BuildingBlocks.Polly;
|
||||||
|
|
||||||
|
public class RetryOptions
|
||||||
|
{
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
public int SleepDuration { get; set; }
|
||||||
|
}
|
||||||
@ -408,6 +408,14 @@ public class TestReadFixture<TEntryPoint, TRContext> : TestFixture<TEntryPoint>
|
|||||||
{
|
{
|
||||||
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
|
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) where T : class
|
||||||
|
{
|
||||||
|
await ExecuteReadContextAsync(async db =>
|
||||||
|
{
|
||||||
|
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<TEntryPoint, TWContext>
|
public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<TEntryPoint, TWContext>
|
||||||
@ -424,6 +432,14 @@ public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<T
|
|||||||
{
|
{
|
||||||
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
|
return ExecuteScopeAsync(sp => action(sp.GetRequiredService<TRContext>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task InsertMongoDbContextAsync<T>(string collectionName, params T[] entities) where T : class
|
||||||
|
{
|
||||||
|
await ExecuteReadContextAsync(async db =>
|
||||||
|
{
|
||||||
|
await db.GetCollection<T>(collectionName).InsertManyAsync(entities.ToList());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
|
public class TestFixtureCore<TEntryPoint> : IAsyncLifetime
|
||||||
|
|||||||
@ -36,6 +36,16 @@
|
|||||||
"FlightAddress": "https://localhost:5003",
|
"FlightAddress": "https://localhost:5003",
|
||||||
"PassengerAddress": "https://localhost:5012"
|
"PassengerAddress": "https://localhost:5012"
|
||||||
},
|
},
|
||||||
|
"RetryOptions": {
|
||||||
|
"Retry": {
|
||||||
|
"RetryCount": 3,
|
||||||
|
"SleepDuration": 1
|
||||||
|
},
|
||||||
|
"CircuitBreaker": {
|
||||||
|
"RetryCount": 5,
|
||||||
|
"BreakDuration" : 30
|
||||||
|
}
|
||||||
|
},
|
||||||
"EventStore": {
|
"EventStore": {
|
||||||
"ConnectionString": "esdb://localhost:2113?tls=false"
|
"ConnectionString": "esdb://localhost:2113?tls=false"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,6 +6,8 @@ using Passenger;
|
|||||||
|
|
||||||
namespace Booking.Extensions.Infrastructure;
|
namespace Booking.Extensions.Infrastructure;
|
||||||
|
|
||||||
|
using BuildingBlocks.Polly;
|
||||||
|
|
||||||
public static class GrpcClientExtensions
|
public static class GrpcClientExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddGrpcClients(this IServiceCollection services)
|
public static IServiceCollection AddGrpcClients(this IServiceCollection services)
|
||||||
@ -15,12 +17,16 @@ public static class GrpcClientExtensions
|
|||||||
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
|
services.AddGrpcClient<FlightGrpcService.FlightGrpcServiceClient>(o =>
|
||||||
{
|
{
|
||||||
o.Address = new Uri(grpcOptions.FlightAddress);
|
o.Address = new Uri(grpcOptions.FlightAddress);
|
||||||
});
|
})
|
||||||
|
.AddGrpcRetryPolicyHandler()
|
||||||
|
.AddGrpcCircuitBreakerPolicyHandler();
|
||||||
|
|
||||||
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
|
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
|
||||||
{
|
{
|
||||||
o.Address = new Uri(grpcOptions.PassengerAddress);
|
o.Address = new Uri(grpcOptions.PassengerAddress);
|
||||||
});
|
})
|
||||||
|
.AddGrpcRetryPolicyHandler()
|
||||||
|
.AddGrpcCircuitBreakerPolicyHandler();;
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ public static class InfrastructureExtensions
|
|||||||
|
|
||||||
SnowFlakIdGenerator.Configure(3);
|
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)
|
builder.Services.AddEventStore(configuration, typeof(BookingRoot).Assembly)
|
||||||
.AddEventStoreDBSubscriptionToAll();
|
.AddEventStoreDBSubscriptionToAll();
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
namespace Booking.Extensions.Infrastructure;
|
namespace Booking.Extensions.Infrastructure;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public static class ProblemDetailsExtensions
|
public static class ProblemDetailsExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
||||||
@ -74,6 +76,14 @@ public static class ProblemDetailsExtensions
|
|||||||
Type = "https://somedomain/grpc-error"
|
Type = "https://somedomain/grpc-error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
x.Map<DbUpdateConcurrencyException>(ex => new ProblemDetailsWithCode
|
||||||
|
{
|
||||||
|
Title = ex.GetType().Name,
|
||||||
|
Status = StatusCodes.Status409Conflict,
|
||||||
|
Detail = ex.Message,
|
||||||
|
Type = "https://somedomain/db-update-concurrency-error"
|
||||||
|
});
|
||||||
|
|
||||||
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
x.MapStatusCode = context =>
|
x.MapStatusCode = context =>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ namespace Flight.Data
|
|||||||
|
|
||||||
builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true")
|
builder.UseNpgsql("Server=localhost;Port=5432;Database=flight;User Id=postgres;Password=postgres;Include Error Detail=true")
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
return new FlightDbContext(builder.Options, null);
|
return new FlightDbContext(builder.Options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using BuildingBlocks.EFCore;
|
using BuildingBlocks.EFCore;
|
||||||
using BuildingBlocks.Web;
|
|
||||||
using Flight.Aircrafts.Models;
|
using Flight.Aircrafts.Models;
|
||||||
using Flight.Airports.Models;
|
using Flight.Airports.Models;
|
||||||
using Flight.Seats.Models;
|
using Flight.Seats.Models;
|
||||||
@ -7,10 +6,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace Flight.Data;
|
namespace Flight.Data;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
public sealed class FlightDbContext : AppDbContextBase
|
public sealed class FlightDbContext : AppDbContextBase
|
||||||
{
|
{
|
||||||
public FlightDbContext(DbContextOptions<FlightDbContext> options, ICurrentUserProvider currentUserProvider) : base(
|
public FlightDbContext(DbContextOptions<FlightDbContext> options, IHttpContextAccessor httpContextAccessor = default) : base(
|
||||||
options, currentUserProvider)
|
options, httpContextAccessor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
namespace Flight.Extensions.Infrastructure;
|
namespace Flight.Extensions.Infrastructure;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public static class ProblemDetailsExtensions
|
public static class ProblemDetailsExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
||||||
@ -65,6 +67,14 @@ public static class ProblemDetailsExtensions
|
|||||||
Type = "https://somedomain/application-error"
|
Type = "https://somedomain/application-error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
x.Map<DbUpdateConcurrencyException>(ex => new ProblemDetailsWithCode
|
||||||
|
{
|
||||||
|
Title = ex.GetType().Name,
|
||||||
|
Status = StatusCodes.Status409Conflict,
|
||||||
|
Detail = ex.Message,
|
||||||
|
Type = "https://somedomain/db-update-concurrency-error"
|
||||||
|
});
|
||||||
|
|
||||||
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
x.MapStatusCode = context =>
|
x.MapStatusCode = context =>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ namespace Unit.Test.Common
|
|||||||
var options = new DbContextOptionsBuilder<FlightDbContext>()
|
var options = new DbContextOptionsBuilder<FlightDbContext>()
|
||||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
|
||||||
|
|
||||||
var context = new FlightDbContext(options, currentUserProvider: null);
|
var context = new FlightDbContext(options);
|
||||||
|
|
||||||
// Seed our data
|
// Seed our data
|
||||||
FlightDataSeeder(context);
|
FlightDataSeeder(context);
|
||||||
|
|||||||
@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<IdentityCo
|
|||||||
|
|
||||||
builder.UseNpgsql("Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true")
|
builder.UseNpgsql("Server=localhost;Port=5432;Database=identity;User Id=postgres;Password=postgres;Include Error Detail=true")
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
return new IdentityContext(builder.Options, null);
|
return new IdentityContext(builder.Options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ public sealed class IdentityContext : IdentityDbContext<ApplicationUser, Identit
|
|||||||
{
|
{
|
||||||
private IDbContextTransaction _currentTransaction;
|
private IDbContextTransaction _currentTransaction;
|
||||||
|
|
||||||
public IdentityContext(DbContextOptions<IdentityContext> options, IHttpContextAccessor httpContextAccessor) :
|
public IdentityContext(DbContextOptions<IdentityContext> options, IHttpContextAccessor httpContextAccessor = default) :
|
||||||
base(options)
|
base(options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
namespace Identity.Extensions.Infrastructure;
|
namespace Identity.Extensions.Infrastructure;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public static class ProblemDetailsExtensions
|
public static class ProblemDetailsExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
||||||
@ -65,6 +67,14 @@ public static class ProblemDetailsExtensions
|
|||||||
Type = "https://somedomain/application-error"
|
Type = "https://somedomain/application-error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
x.Map<DbUpdateConcurrencyException>(ex => new ProblemDetailsWithCode
|
||||||
|
{
|
||||||
|
Title = ex.GetType().Name,
|
||||||
|
Status = StatusCodes.Status409Conflict,
|
||||||
|
Detail = ex.Message,
|
||||||
|
Type = "https://somedomain/db-update-concurrency-error"
|
||||||
|
});
|
||||||
|
|
||||||
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
x.MapStatusCode = context =>
|
x.MapStatusCode = context =>
|
||||||
|
|||||||
@ -11,6 +11,6 @@ public class DesignTimeDbContextFactory: IDesignTimeDbContextFactory<PassengerDb
|
|||||||
|
|
||||||
builder.UseNpgsql("Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true")
|
builder.UseNpgsql("Server=localhost;Port=5432;Database=passenger;User Id=postgres;Password=postgres;Include Error Detail=true")
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
return new PassengerDbContext(builder.Options, null);
|
return new PassengerDbContext(builder.Options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using BuildingBlocks.EFCore;
|
using BuildingBlocks.EFCore;
|
||||||
using BuildingBlocks.Web;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Passenger.Data;
|
namespace Passenger.Data;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
public sealed class PassengerDbContext : AppDbContextBase
|
public sealed class PassengerDbContext : AppDbContextBase
|
||||||
{
|
{
|
||||||
public PassengerDbContext(DbContextOptions<PassengerDbContext> options, ICurrentUserProvider currentUserProvider) : base(options, currentUserProvider)
|
public PassengerDbContext(DbContextOptions<PassengerDbContext> options,
|
||||||
|
IHttpContextAccessor httpContextAccessor = default) :
|
||||||
|
base(options, httpContextAccessor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ using Microsoft.Extensions.Hosting;
|
|||||||
|
|
||||||
namespace Passenger.Extensions.Infrastructure;
|
namespace Passenger.Extensions.Infrastructure;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
public static class ProblemDetailsExtensions
|
public static class ProblemDetailsExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services)
|
||||||
@ -64,6 +66,14 @@ public static class ProblemDetailsExtensions
|
|||||||
Type = "https://somedomain/application-error"
|
Type = "https://somedomain/application-error"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
x.Map<DbUpdateConcurrencyException>(ex => new ProblemDetailsWithCode
|
||||||
|
{
|
||||||
|
Title = ex.GetType().Name,
|
||||||
|
Status = StatusCodes.Status409Conflict,
|
||||||
|
Detail = ex.Message,
|
||||||
|
Type = "https://somedomain/db-update-concurrency-error"
|
||||||
|
});
|
||||||
|
|
||||||
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
x.MapToStatusCode<ArgumentNullException>(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
x.MapStatusCode = context =>
|
x.MapStatusCode = context =>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user