Merge pull request #241 from meysamhadeli/feat/Use-builtin-problem-details-dotnet-7

feat: Use built-in problem-details .Net 7 instead of Hellang.Middlewa…
This commit is contained in:
Meysam Hadeli 2023-03-31 01:26:58 +03:30 committed by GitHub
commit e1d99a6f24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 398 additions and 597 deletions

View File

@ -44,7 +44,6 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Hellang.Middleware.ProblemDetails" Version="6.5.1" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="IdGen" Version="3.0.3" /> <PackageReference Include="IdGen" Version="3.0.3" />
<PackageReference Include="Mapster" Version="7.3.0" /> <PackageReference Include="Mapster" Version="7.3.0" />

View File

@ -1,7 +1,6 @@
namespace Booking.Booking.Features.CreatingBook.Commands.V1; namespace Booking.Booking.Features.CreatingBook.Commands.V1;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -18,25 +17,11 @@ public class CreateBookingEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/booking", CreateBooking) builder.MapPost($"{EndpointConfig.BaseApiPath}/booking", CreateBooking)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Booking")
.WithName("CreateBooking") .WithName("CreateBooking")
.WithMetadata(new SwaggerOperationAttribute("Create Booking", "Create Booking")) .WithMetadata(new SwaggerOperationAttribute("Create Booking", "Create Booking"))
.WithApiVersionSet(builder.NewApiVersionSet("Booking").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Booking").Build())
.WithMetadata( .Produces<CreateBookingResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"Booking Created",
typeof(CreateBookingResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,7 +3,6 @@ using Booking.Data;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EventStoreDB; using BuildingBlocks.EventStoreDB;
using BuildingBlocks.HealthCheck; using BuildingBlocks.HealthCheck;
using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Jwt; using BuildingBlocks.Jwt;
using BuildingBlocks.Logging; using BuildingBlocks.Logging;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
@ -15,7 +14,6 @@ using BuildingBlocks.Swagger;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using FluentValidation; using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -72,7 +70,7 @@ public static class InfrastructureExtensions
builder.Services.AddCustomVersioning(); builder.Services.AddCustomVersioning();
builder.Services.AddCustomMediatR(); builder.Services.AddCustomMediatR();
builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(BookingRoot).Assembly);
builder.Services.AddCustomProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly); builder.Services.AddCustomMapster(typeof(BookingRoot).Assembly);
builder.Services.AddCustomHealthCheck(); builder.Services.AddCustomHealthCheck();
builder.Services.AddCustomMassTransit(env, typeof(BookingRoot).Assembly); builder.Services.AddCustomMassTransit(env, typeof(BookingRoot).Assembly);
@ -94,7 +92,7 @@ public static class InfrastructureExtensions
var env = app.Environment; var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions)); var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseProblemDetails(); app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options => app.UseSerilogRequestLogging(options =>
{ {
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest; options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;

View File

@ -1,107 +1,106 @@
using BuildingBlocks.Exception;
using Grpc.Core;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Booking.Extensions.Infrastructure; namespace Booking.Extensions.Infrastructure;
using BuildingBlocks.Exception;
using Grpc.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public static class ProblemDetailsExtensions public static class ProblemDetailsExtensions
{ {
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) public static WebApplication UseCustomProblemDetails(this WebApplication app)
{ {
services.AddProblemDetails(x => app.UseExceptionHandler(exceptionHandlerApp =>
{ {
// Control when an exception is included exceptionHandlerApp.Run(async context =>
x.IncludeExceptionDetails = (ctx, _) =>
{ {
// Fetch services from HttpContext.RequestServices context.Response.ContentType = "application/problem+json";
var env = ctx.RequestServices.GetRequiredService<IHostEnvironment>();
return env.IsDevelopment() || env.IsStaging();
};
x.Map<ConflictException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status409Conflict,
Detail = ex.Message,
Type = "https://somedomain/application-rule-validation-error",
});
// Exception will produce and returns from our FluentValidation RequestValidationBehavior if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
x.Map<ValidationException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = (int)ex.StatusCode,
Detail = ex.Message,
Type = "https://somedomain/input-validation-rules-error",
});
x.Map<BadRequestException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
Type = "https://somedomain/bad-request-error",
});
x.Map<NotFoundException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status404NotFound,
Detail = ex.Message,
Type = "https://somedomain/not-found-error",
});
x.Map<InternalServerException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status500InternalServerError,
Detail = ex.Message,
Type = "https://somedomain/api-server-error",
});
x.Map<AppException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
Type = "https://somedomain/application-error",
});
x.Map<RpcException>(ex => new ProblemDetails
{
Status = StatusCodes.Status400BadRequest,
Title = ex.GetType().Name,
Detail = ex.Status.Detail,
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 =>
{
return context.Response.StatusCode switch
{ {
StatusCodes.Status401Unauthorized => new ProblemDetailsWithCode var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
{ var exceptionType = exceptionHandlerFeature?.Error;
Status = context.Response.StatusCode,
Title = "identity exception",
Detail = "You are not Authorized",
Type = "https://somedomain/identity-error",
},
_ => new StatusCodeProblemDetails(context.Response.StatusCode) if (exceptionType is not null)
}; {
}; (string Detail, string Type, string Title, int StatusCode) details = exceptionType switch
{
ConflictException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
ValidationException validationException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = (int)validationException.StatusCode
),
BadRequestException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
NotFoundException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status404NotFound
),
AppException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
DbUpdateConcurrencyException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
RpcException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
_ =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status500InternalServerError
)
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = details.Title,
Detail = details.Detail,
Type = details.Type,
Status = details.StatusCode
}
});
}
}
});
}); });
return services;
return app;
} }
} }

View File

@ -3,8 +3,6 @@ namespace Flight.Aircrafts.Features.CreatingAircraft.V1;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Dtos;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -21,25 +19,11 @@ public class CreateAircraftEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/aircraft", CreateAircraft) builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/aircraft", CreateAircraft)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("CreateAircraft") .WithName("CreateAircraft")
.WithMetadata(new SwaggerOperationAttribute("Create Aircraft", "Create Aircraft")) .WithMetadata(new SwaggerOperationAttribute("Create Aircraft", "Create Aircraft"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<CreateAircraftResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"Aircraft Created",
typeof(CreateAircraftResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,8 +3,6 @@ namespace Flight.Airports.Features.CreatingAirport.V1;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Dtos;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -21,25 +19,11 @@ public class CreateAirportEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/airport", CreateAirport) builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/airport", CreateAirport)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("CreateAirport") .WithName("CreateAirport")
.WithMetadata(new SwaggerOperationAttribute("Create Airport", "Create Airport")) .WithMetadata(new SwaggerOperationAttribute("Create Airport", "Create Airport"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<CreateAirportResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"Airport Created",
typeof(CreateAirportResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -19,7 +19,6 @@ using Flight.Data;
using Flight.Data.Seed; using Flight.Data.Seed;
using Flight.GrpcServer.Services; using Flight.GrpcServer.Services;
using FluentValidation; using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -51,7 +50,7 @@ public static class InfrastructureExtensions
}); });
builder.Services.AddCustomMediatR(); builder.Services.AddCustomMediatR();
builder.Services.AddCustomProblemDetails(); builder.Services.AddProblemDetails();
var appOptions = builder.Services.GetOptions<AppOptions>(nameof(AppOptions)); var appOptions = builder.Services.GetOptions<AppOptions>(nameof(AppOptions));
Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name)); Console.WriteLine(FiggleFonts.Standard.Render(appOptions.Name));
@ -99,7 +98,7 @@ public static class InfrastructureExtensions
var env = app.Environment; var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions)); var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseProblemDetails(); app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options => app.UseSerilogRequestLogging(options =>
{ {
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest; options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;

View File

@ -1,98 +1,107 @@
using System;
using BuildingBlocks.Exception; using BuildingBlocks.Exception;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Flight.Extensions.Infrastructure; namespace Flight.Extensions.Infrastructure;
using Grpc.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
public static class ProblemDetailsExtensions public static class ProblemDetailsExtensions
{ {
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) public static WebApplication UseCustomProblemDetails(this WebApplication app)
{ {
services.AddProblemDetails(x => app.UseExceptionHandler(exceptionHandlerApp =>
{ {
// Control when an exception is included exceptionHandlerApp.Run(async context =>
x.IncludeExceptionDetails = (ctx, _) =>
{ {
// Fetch services from HttpContext.RequestServices context.Response.ContentType = "application/problem+json";
var env = ctx.RequestServices.GetRequiredService<IHostEnvironment>();
return env.IsDevelopment() || env.IsStaging();
};
x.Map<ConflictException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status409Conflict,
Detail = ex.Message,
Type = "https://somedomain/application-rule-validation-error"
});
// Exception will produce and returns from our FluentValidation RequestValidationBehavior if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
x.Map<ValidationException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = (int)ex.StatusCode,
Detail = ex.Message,
Type = "https://somedomain/input-validation-rules-error"
});
x.Map<BadRequestException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
Type = "https://somedomain/bad-request-error"
});
x.Map<NotFoundException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status404NotFound,
Detail = ex.Message,
Type = "https://somedomain/not-found-error"
});
x.Map<InternalServerException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status500InternalServerError,
Detail = ex.Message,
Type = "https://somedomain/api-server-error"
});
x.Map<AppException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
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 =>
{
return context.Response.StatusCode switch
{ {
StatusCodes.Status401Unauthorized => new ProblemDetailsWithCode var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
{ var exceptionType = exceptionHandlerFeature?.Error;
Status = context.Response.StatusCode,
Title = "identity exception",
Detail = "You are not Authorized",
Type = "https://somedomain/identity-error"
},
_ => new StatusCodeProblemDetails(context.Response.StatusCode) if (exceptionType is not null)
}; {
}; (string Detail, string Type, string Title, int StatusCode) details = exceptionType switch
{
ConflictException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
ValidationException validationException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = (int)validationException.StatusCode
),
BadRequestException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
NotFoundException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status404NotFound
),
AppException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
DbUpdateConcurrencyException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
RpcException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
_ =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status500InternalServerError
)
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = details.Title,
Detail = details.Detail,
Type = details.Type,
Status = details.StatusCode
}
});
}
}
});
}); });
return services;
return app;
} }
} }

View File

@ -4,7 +4,6 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -24,25 +23,11 @@ public class CreateFlightEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight", CreateFlight) builder.MapPost($"{EndpointConfig.BaseApiPath}/flight", CreateFlight)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("CreateFlight") .WithName("CreateFlight")
.WithMetadata(new SwaggerOperationAttribute("Create Flight", "Create Flight")) .WithMetadata(new SwaggerOperationAttribute("Create Flight", "Create Flight"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<CreateFlightResponseDto>(StatusCodes.Status201Created)
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status201Created,
"Flight Created",
typeof(CreateFlightResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,8 +3,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Flight.Flights.Dtos;
using Hellang.Middleware.ProblemDetails;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -17,24 +15,11 @@ public class DeleteFlightEndpoint : IMinimalEndpoint
{ {
builder.MapDelete($"{EndpointConfig.BaseApiPath}/flight/{{id}}", DeleteFlight) builder.MapDelete($"{EndpointConfig.BaseApiPath}/flight/{{id}}", DeleteFlight)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("DeleteFlight") .WithName("DeleteFlight")
.WithMetadata(new SwaggerOperationAttribute("Delete Flight", "Delete Flight")) .WithMetadata(new SwaggerOperationAttribute("Delete Flight", "Delete Flight"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces(StatusCodes.Status204NoContent)
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status204NoContent,
"Flight Deleted"))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -5,7 +5,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Dtos; using Dtos;
using Hellang.Middleware.ProblemDetails;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -20,25 +19,11 @@ public class GetAvailableFlightsEndpoint : IMinimalEndpoint
{ {
builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-flights", GetAvailableFlights) builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-flights", GetAvailableFlights)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("GetAvailableFlights") .WithName("GetAvailableFlights")
.WithMetadata(new SwaggerOperationAttribute("Get Available Flights", "Get Available Flights")) .WithMetadata(new SwaggerOperationAttribute("Get Available Flights", "Get Available Flights"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<GetAvailableFlightsResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"GetAvailableFlights",
typeof(GetAvailableFlightsResult)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -4,7 +4,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Dtos; using Dtos;
using Hellang.Middleware.ProblemDetails;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -19,28 +18,11 @@ public class GetFlightByIdEndpoint : IMinimalEndpoint
{ {
builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/{{id}}", GetById) builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/{{id}}", GetById)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("GetFlightById") .WithName("GetFlightById")
.WithMetadata(new SwaggerOperationAttribute("Get Flight By Id", "Get Flight By Id")) .WithMetadata(new SwaggerOperationAttribute("Get Flight By Id", "Get Flight By Id"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<FlightDto>() .Produces<GetFlightByIdResponseDto>()
.Produces(StatusCodes.Status200OK) .ProducesProblem(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status400BadRequest)
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status200OK,
"GetFlightById",
typeof(GetFlightByIdResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -4,8 +4,6 @@ using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Dtos;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -22,27 +20,11 @@ public class UpdateFlightEndpoint : IMinimalEndpoint
{ {
builder.MapPut($"{EndpointConfig.BaseApiPath}/flight", UpdateFlight) builder.MapPut($"{EndpointConfig.BaseApiPath}/flight", UpdateFlight)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("UpdateFlight") .WithName("UpdateFlight")
.WithMetadata(new SwaggerOperationAttribute("Update Flight", "Update Flight")) .WithMetadata(new SwaggerOperationAttribute("Update Flight", "Update Flight"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.Produces<FlightDto>()
.Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status400BadRequest) .ProducesProblem(StatusCodes.Status400BadRequest)
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status204NoContent,
"Flight Updated"))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,8 +3,6 @@ namespace Flight.Seats.Features.CreatingSeat.V1;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Flight.Seats.Dtos;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -21,25 +19,11 @@ public class CreateSeatEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/seat", CreateSeat) builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/seat", CreateSeat)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("CreateSeat") .WithName("CreateSeat")
.WithMetadata(new SwaggerOperationAttribute("Create Seat", "Create Seat")) .WithMetadata(new SwaggerOperationAttribute("Create Seat", "Create Seat"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<CreateSeatResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"Seat Created",
typeof(CreateSeatResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -4,8 +4,7 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Flight.Seats.Dtos; using Dtos;
using Hellang.Middleware.ProblemDetails;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -20,25 +19,11 @@ public class GetAvailableSeatsEndpoint : IMinimalEndpoint
{ {
builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-seats/{{id}}", GetAvailableSeats) builder.MapGet($"{EndpointConfig.BaseApiPath}/flight/get-available-seats/{{id}}", GetAvailableSeats)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("GetAvailableSeats") .WithName("GetAvailableSeats")
.WithMetadata(new SwaggerOperationAttribute("Get Available Seats", "Get Available Seats")) .WithMetadata(new SwaggerOperationAttribute("Get Available Seats", "Get Available Seats"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<GetAvailableSeatsResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"GetAvailableSeats",
typeof(GetAvailableSeatsResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,8 +3,6 @@ namespace Flight.Seats.Features.ReservingSeat.Commands.V1;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Flight.Seats.Dtos;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -21,25 +19,11 @@ public class ReserveSeatEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/reserve-seat", ReserveSeat) builder.MapPost($"{EndpointConfig.BaseApiPath}/flight/reserve-seat", ReserveSeat)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Flight")
.WithName("ReserveSeat") .WithName("ReserveSeat")
.WithMetadata(new SwaggerOperationAttribute("Reserve Seat", "Reserve Seat")) .WithMetadata(new SwaggerOperationAttribute("Reserve Seat", "Reserve Seat"))
.WithApiVersionSet(builder.NewApiVersionSet("Flight").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Flight").Build())
.WithMetadata( .Produces<ReserveSeatResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"ReserveSeat",
typeof(ReserveSeatResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,7 +3,6 @@ using System.Threading.RateLimiting;
using BuildingBlocks.Core; using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.HealthCheck; using BuildingBlocks.HealthCheck;
using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Logging; using BuildingBlocks.Logging;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
using BuildingBlocks.MassTransit; using BuildingBlocks.MassTransit;
@ -13,7 +12,6 @@ using BuildingBlocks.Swagger;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using FluentValidation; using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Identity.Data; using Identity.Data;
using Identity.Data.Seed; using Identity.Data.Seed;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -72,7 +70,7 @@ public static class InfrastructureExtensions
builder.Services.AddCustomVersioning(); builder.Services.AddCustomVersioning();
builder.Services.AddCustomMediatR(); builder.Services.AddCustomMediatR();
builder.Services.AddValidatorsFromAssembly(typeof(IdentityRoot).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(IdentityRoot).Assembly);
builder.Services.AddCustomProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly); builder.Services.AddCustomMapster(typeof(IdentityRoot).Assembly);
builder.Services.AddCustomHealthCheck(); builder.Services.AddCustomHealthCheck();
@ -98,7 +96,7 @@ public static class InfrastructureExtensions
app.UseForwardedHeaders(); app.UseForwardedHeaders();
app.UseProblemDetails(); app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options => app.UseSerilogRequestLogging(options =>
{ {
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest; options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;
@ -107,7 +105,6 @@ public static class InfrastructureExtensions
app.UseMigration<IdentityContext>(env); app.UseMigration<IdentityContext>(env);
app.UseCorrelationId(); app.UseCorrelationId();
app.UseHttpMetrics(); app.UseHttpMetrics();
app.UseProblemDetails();
app.UseCustomHealthCheck(); app.UseCustomHealthCheck();
app.UseIdentityServer(); app.UseIdentityServer();
app.MapMetrics(); app.MapMetrics();

View File

@ -1,98 +1,106 @@
using System;
using BuildingBlocks.Exception;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Identity.Extensions.Infrastructure; namespace Identity.Extensions.Infrastructure;
using BuildingBlocks.Exception;
using Grpc.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public static class ProblemDetailsExtensions public static class ProblemDetailsExtensions
{ {
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) public static WebApplication UseCustomProblemDetails(this WebApplication app)
{ {
services.AddProblemDetails(x => app.UseExceptionHandler(exceptionHandlerApp =>
{ {
// Control when an exception is included exceptionHandlerApp.Run(async context =>
x.IncludeExceptionDetails = (ctx, _) =>
{ {
// Fetch services from HttpContext.RequestServices context.Response.ContentType = "application/problem+json";
var env = ctx.RequestServices.GetRequiredService<IHostEnvironment>();
return env.IsDevelopment() || env.IsStaging();
};
x.Map<ConflictException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status409Conflict,
Detail = ex.Message,
Type = "https://somedomain/application-rule-validation-error"
});
// Exception will produce and returns from our FluentValidation RequestValidationBehavior if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
x.Map<ValidationException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = (int)ex.StatusCode,
Detail = ex.Message,
Type = "https://somedomain/input-validation-rules-error"
});
x.Map<BadRequestException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
Type = "https://somedomain/bad-request-error"
});
x.Map<NotFoundException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status404NotFound,
Detail = ex.Message,
Type = "https://somedomain/not-found-error"
});
x.Map<InternalServerException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status500InternalServerError,
Detail = ex.Message,
Type = "https://somedomain/api-server-error"
});
x.Map<AppException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
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 =>
{
return context.Response.StatusCode switch
{ {
StatusCodes.Status401Unauthorized => new ProblemDetailsWithCode var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
{ var exceptionType = exceptionHandlerFeature?.Error;
Status = context.Response.StatusCode,
Title = "identity exception",
Detail = "You are not Authorized",
Type = "https://somedomain/identity-error"
},
_ => new StatusCodeProblemDetails(context.Response.StatusCode) if (exceptionType is not null)
}; {
}; (string Detail, string Type, string Title, int StatusCode) details = exceptionType switch
{
ConflictException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
ValidationException validationException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = (int)validationException.StatusCode
),
BadRequestException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
NotFoundException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status404NotFound
),
AppException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
DbUpdateConcurrencyException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
RpcException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
_ =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status500InternalServerError
)
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = details.Title,
Detail = details.Detail,
Type = details.Type,
Status = details.StatusCode
}
});
}
}
});
}); });
return services;
return app;
} }
} }

View File

@ -3,7 +3,6 @@ namespace Identity.Identity.Features.RegisteringNewUser.V1;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -21,20 +20,11 @@ public class RegisterNewUserEndpoint : IMinimalEndpoint
public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder) public IEndpointRouteBuilder MapEndpoint(IEndpointRouteBuilder builder)
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/identity/register-user", RegisterNewUser) builder.MapPost($"{EndpointConfig.BaseApiPath}/identity/register-user", RegisterNewUser)
.WithTags("Identity")
.WithName("RegisterUser") .WithName("RegisterUser")
.WithMetadata(new SwaggerOperationAttribute("Register User", "Register User")) .WithMetadata(new SwaggerOperationAttribute("Register User", "Register User"))
.WithApiVersionSet(builder.NewApiVersionSet("Identity").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Identity").Build())
.WithMetadata( .Produces<RegisterNewUserResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"User Registered",
typeof(RegisterNewUserResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -3,7 +3,6 @@ using BuildingBlocks.Core;
using BuildingBlocks.EFCore; using BuildingBlocks.EFCore;
using BuildingBlocks.Exception; using BuildingBlocks.Exception;
using BuildingBlocks.HealthCheck; using BuildingBlocks.HealthCheck;
using BuildingBlocks.IdsGenerator;
using BuildingBlocks.Jwt; using BuildingBlocks.Jwt;
using BuildingBlocks.Logging; using BuildingBlocks.Logging;
using BuildingBlocks.Mapster; using BuildingBlocks.Mapster;
@ -15,7 +14,6 @@ using BuildingBlocks.Swagger;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Figgle; using Figgle;
using FluentValidation; using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -73,7 +71,7 @@ public static class InfrastructureExtensions
builder.Services.AddCustomVersioning(); builder.Services.AddCustomVersioning();
builder.Services.AddCustomMediatR(); builder.Services.AddCustomMediatR();
builder.Services.AddValidatorsFromAssembly(typeof(PassengerRoot).Assembly); builder.Services.AddValidatorsFromAssembly(typeof(PassengerRoot).Assembly);
builder.Services.AddCustomProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly); builder.Services.AddCustomMapster(typeof(PassengerRoot).Assembly);
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
builder.Services.AddCustomHealthCheck(); builder.Services.AddCustomHealthCheck();
@ -93,7 +91,7 @@ public static class InfrastructureExtensions
var env = app.Environment; var env = app.Environment;
var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions)); var appOptions = app.GetOptions<AppOptions>(nameof(AppOptions));
app.UseProblemDetails(); app.UseCustomProblemDetails();
app.UseSerilogRequestLogging(options => app.UseSerilogRequestLogging(options =>
{ {
options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest; options.EnrichDiagnosticContext = LogEnrichHelper.EnrichFromRequest;

View File

@ -1,97 +1,106 @@
using BuildingBlocks.Exception;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Passenger.Extensions.Infrastructure; namespace Passenger.Extensions.Infrastructure;
using BuildingBlocks.Exception;
using Grpc.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public static class ProblemDetailsExtensions public static class ProblemDetailsExtensions
{ {
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services) public static WebApplication UseCustomProblemDetails(this WebApplication app)
{ {
services.AddProblemDetails(x => app.UseExceptionHandler(exceptionHandlerApp =>
{ {
// Control when an exception is included exceptionHandlerApp.Run(async context =>
x.IncludeExceptionDetails = (ctx, _) =>
{ {
// Fetch services from HttpContext.RequestServices context.Response.ContentType = "application/problem+json";
var env = ctx.RequestServices.GetRequiredService<IHostEnvironment>();
return env.IsDevelopment() || env.IsStaging();
};
x.Map<ConflictException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status409Conflict,
Detail = ex.Message,
Type = "https://somedomain/application-rule-validation-error"
});
// Exception will produce and returns from our FluentValidation RequestValidationBehavior if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
x.Map<ValidationException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = (int)ex.StatusCode,
Detail = ex.Message,
Type = "https://somedomain/input-validation-rules-error"
});
x.Map<BadRequestException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
Type = "https://somedomain/bad-request-error"
});
x.Map<NotFoundException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status404NotFound,
Detail = ex.Message,
Type = "https://somedomain/not-found-error"
});
x.Map<InternalServerException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status500InternalServerError,
Detail = ex.Message,
Type = "https://somedomain/api-server-error"
});
x.Map<AppException>(ex => new ProblemDetailsWithCode
{
Title = ex.GetType().Name,
Status = StatusCodes.Status400BadRequest,
Detail = ex.Message,
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 =>
{
return context.Response.StatusCode switch
{ {
StatusCodes.Status401Unauthorized => new ProblemDetailsWithCode var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
{ var exceptionType = exceptionHandlerFeature?.Error;
Status = context.Response.StatusCode,
Title = "identity exception",
Detail = "You are not Authorized",
Type = "https://somedomain/identity-error"
},
_ => new StatusCodeProblemDetails(context.Response.StatusCode) if (exceptionType is not null)
}; {
}; (string Detail, string Type, string Title, int StatusCode) details = exceptionType switch
{
ConflictException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
ValidationException validationException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = (int)validationException.StatusCode
),
BadRequestException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
NotFoundException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status404NotFound
),
AppException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
DbUpdateConcurrencyException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status409Conflict
),
RpcException =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status400BadRequest
),
_ =>
(
exceptionType.Message,
exceptionType.GetType().ToString(),
exceptionType.GetType().Name,
context.Response.StatusCode = StatusCodes.Status500InternalServerError
)
};
await problemDetailsService.WriteAsync(new ProblemDetailsContext
{
HttpContext = context,
ProblemDetails =
{
Title = details.Title,
Detail = details.Detail,
Type = details.Type,
Status = details.StatusCode
}
});
}
}
});
}); });
return services;
return app;
} }
} }

View File

@ -1,7 +1,6 @@
namespace Passenger.Passengers.Features.CompletingRegisterPassenger.V1; namespace Passenger.Passengers.Features.CompletingRegisterPassenger.V1;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Hellang.Middleware.ProblemDetails;
using MapsterMapper; using MapsterMapper;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -19,25 +18,11 @@ public class CompleteRegisterPassengerEndpoint : IMinimalEndpoint
{ {
builder.MapPost($"{EndpointConfig.BaseApiPath}/passenger/complete-registration", CompleteRegisterPassenger) builder.MapPost($"{EndpointConfig.BaseApiPath}/passenger/complete-registration", CompleteRegisterPassenger)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Passenger")
.WithName("CompleteRegisterPassenger") .WithName("CompleteRegisterPassenger")
.WithMetadata(new SwaggerOperationAttribute("Complete Register Passenger", "Complete Register Passenger")) .WithMetadata(new SwaggerOperationAttribute("Complete Register Passenger", "Complete Register Passenger"))
.WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build())
.WithMetadata( .Produces<CompleteRegisterPassengerResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"Register Passenger Completed",
typeof(CompleteRegisterPassengerResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;

View File

@ -1,12 +1,11 @@
namespace Passenger.Passengers.Features.GettingPassengerById.Queries.V1; namespace Passenger.Passengers.Features.GettingPassengerById.Queries.V1;
using BuildingBlocks.Web; using BuildingBlocks.Web;
using Hellang.Middleware.ProblemDetails;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Passenger.Passengers.Dtos; using Dtos;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
public record GetPassengerByIdResponseDto(PassengerDto PassengerDto); public record GetPassengerByIdResponseDto(PassengerDto PassengerDto);
@ -17,25 +16,11 @@ public class GetPassengerByIdEndpoint : IMinimalEndpoint
{ {
builder.MapGet($"{EndpointConfig.BaseApiPath}/passenger/{{id}}", GetById) builder.MapGet($"{EndpointConfig.BaseApiPath}/passenger/{{id}}", GetById)
.RequireAuthorization() .RequireAuthorization()
.WithTags("Passenger")
.WithName("GetPassengerById") .WithName("GetPassengerById")
.WithMetadata(new SwaggerOperationAttribute("Get Passenger By Id", "Get Passenger By Id")) .WithMetadata(new SwaggerOperationAttribute("Get Passenger By Id", "Get Passenger By Id"))
.WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build()) .WithApiVersionSet(builder.NewApiVersionSet("Passenger").Build())
.WithMetadata( .Produces<GetPassengerByIdResponseDto>()
new SwaggerResponseAttribute( .ProducesProblem(StatusCodes.Status400BadRequest)
StatusCodes.Status200OK,
"GetPassengerById",
typeof(GetPassengerByIdResponseDto)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status400BadRequest,
"BadRequest",
typeof(StatusCodeProblemDetails)))
.WithMetadata(
new SwaggerResponseAttribute(
StatusCodes.Status401Unauthorized,
"UnAuthorized",
typeof(StatusCodeProblemDetails)))
.HasApiVersion(1.0); .HasApiVersion(1.0);
return builder; return builder;