mirror of
https://github.com/meysamhadeli/booking-microservices.git
synced 2026-04-13 12:15:46 +08:00
commit
c09f854b28
@ -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
|
||||
|
||||
@ -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<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
public override async Task<int> 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<ILogger<AppDbContextBase>>();
|
||||
|
||||
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<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>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
public class PersistMessageDbContext : AppDbContextBase, IPersistMessageDbContext
|
||||
{
|
||||
public PersistMessageDbContext(DbContextOptions<PersistMessageDbContext> options)
|
||||
: base(options)
|
||||
public PersistMessageDbContext(DbContextOptions<PersistMessageDbContext> options, IHttpContextAccessor httpContextAccessor = default)
|
||||
: base(options, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -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<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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>()));
|
||||
}
|
||||
|
||||
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>
|
||||
@ -424,6 +432,14 @@ public class TestFixture<TEntryPoint, TWContext, TRContext> : TestWriteFixture<T
|
||||
{
|
||||
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
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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<FlightGrpcService.FlightGrpcServiceClient>(o =>
|
||||
{
|
||||
o.Address = new Uri(grpcOptions.FlightAddress);
|
||||
});
|
||||
})
|
||||
.AddGrpcRetryPolicyHandler()
|
||||
.AddGrpcCircuitBreakerPolicyHandler();
|
||||
|
||||
services.AddGrpcClient<PassengerGrpcService.PassengerGrpcServiceClient>(o =>
|
||||
{
|
||||
o.Address = new Uri(grpcOptions.PassengerAddress);
|
||||
});
|
||||
})
|
||||
.AddGrpcRetryPolicyHandler()
|
||||
.AddGrpcCircuitBreakerPolicyHandler();;
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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<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.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")
|
||||
.UseSnakeCaseNamingConvention();
|
||||
return new FlightDbContext(builder.Options, null);
|
||||
return new FlightDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<FlightDbContext> options, ICurrentUserProvider currentUserProvider) : base(
|
||||
options, currentUserProvider)
|
||||
public FlightDbContext(DbContextOptions<FlightDbContext> options, IHttpContextAccessor httpContextAccessor = default) : base(
|
||||
options, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@ -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<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.MapStatusCode = context =>
|
||||
|
||||
@ -18,7 +18,7 @@ namespace Unit.Test.Common
|
||||
var options = new DbContextOptionsBuilder<FlightDbContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
|
||||
|
||||
var context = new FlightDbContext(options, currentUserProvider: null);
|
||||
var context = new FlightDbContext(options);
|
||||
|
||||
// Seed our data
|
||||
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")
|
||||
.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;
|
||||
|
||||
public IdentityContext(DbContextOptions<IdentityContext> options, IHttpContextAccessor httpContextAccessor) :
|
||||
public IdentityContext(DbContextOptions<IdentityContext> options, IHttpContextAccessor httpContextAccessor = default) :
|
||||
base(options)
|
||||
{
|
||||
}
|
||||
|
||||
@ -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<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.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")
|
||||
.UseSnakeCaseNamingConvention();
|
||||
return new PassengerDbContext(builder.Options, null);
|
||||
return new PassengerDbContext(builder.Options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
using System.Reflection;
|
||||
using BuildingBlocks.EFCore;
|
||||
using BuildingBlocks.Web;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Passenger.Data;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
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;
|
||||
|
||||
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<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.MapStatusCode = context =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user