Merge pull request #112 from meysamhadeli/develop

Develop
This commit is contained in:
Meysam Hadeli 2023-01-22 01:10:01 +03:30 committed by GitHub
commit c09f854b28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 455 additions and 69 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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)
{
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,7 @@
namespace BuildingBlocks.Polly;
public class CircuitBreakerOptions
{
public int RetryCount { get; set; }
public int BreakDuration { get; set; }
}

View 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;
}
}
}

View 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;
}
}
}

View 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");
});
});
}
}

View 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);
}
});
});
}
}

View File

@ -0,0 +1,7 @@
namespace BuildingBlocks.Polly;
public class PolicyOptions
{
public RetryOptions Retry { get; set; }
public CircuitBreakerOptions CircuitBreaker { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace BuildingBlocks.Polly;
public class RetryOptions
{
public int RetryCount { get; set; }
public int SleepDuration { get; set; }
}

View File

@ -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

View File

@ -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"
},

View File

@ -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;
}

View File

@ -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();

View File

@ -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 =>

View File

@ -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);
}
}
}

View File

@ -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)
{
}

View File

@ -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 =>

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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)
{
}

View File

@ -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 =>

View File

@ -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);
}
}

View File

@ -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)
{
}

View File

@ -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 =>