Skip to content

Commit

Permalink
✨ Switch to Minimal API
Browse files Browse the repository at this point in the history
  • Loading branch information
jasontaylordev committed Jun 28, 2023
1 parent 93bf064 commit 3847651
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 85 deletions.
2 changes: 1 addition & 1 deletion src/WebUI/ClientApp/src/app/web-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// Generated using the NSwag toolchain v13.17.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------

Expand Down
4 changes: 3 additions & 1 deletion src/WebUI/ConfigureServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Microsoft.AspNetCore.Mvc;
using ZymLabs.NSwag.FluentValidation;

namespace Microsoft.Extensions.DependencyInjection;
namespace CleanArchitecture.WebUI;

public static class ConfigureServices
{
Expand All @@ -29,6 +29,8 @@ public static IServiceCollection AddWebUIServices(this IServiceCollection servic
return new FluentValidationSchemaProcessor(provider, validationRules, loggerFactory);
});

services.AddEndpointsApiExplorer();

// Customise default API behaviour
services.Configure<ApiBehaviorOptions>(options =>
options.SuppressModelStateInvalidFilter = true);
Expand Down
60 changes: 60 additions & 0 deletions src/WebUI/Infrastructure/AbstractEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace CleanArchitecture.WebUI.Infrastructure;

public abstract class AbstractEndpoint : IEndpoint
{
public abstract void Map(WebApplication app);

public string Name => GetType().Name;

public string Group => GetType().Namespace!.Split(".")[^1];

public string BaseRoute => $"/api/{Group}/";

public RouteHandlerBuilder MapGet(WebApplication app, Delegate handler) => MapGet(app, "", handler);

public RouteHandlerBuilder MapGet(WebApplication app, string pattern, Delegate handler, bool withDefaults = true)
{
var builder = app.MapGet(BuildRoutePattern(pattern), handler);

if (withDefaults)
{
builder.WithDefaults(this);
}

return builder;
}

public RouteHandlerBuilder MapPost(WebApplication app, Delegate handler) => MapPost(app, "", handler);

public RouteHandlerBuilder MapPost(WebApplication app, string pattern, Delegate handler)
{
return app.MapPost(BuildRoutePattern(pattern), handler)
.WithDefaults(this);
}

public RouteHandlerBuilder MapPut(WebApplication app, Delegate handler) => MapPut(app, "", handler);

public RouteHandlerBuilder MapPut(WebApplication app, string pattern, Delegate handler)
{
return app.MapPut(BuildRoutePattern(pattern), handler)
.WithDefaults(this);
}

public RouteHandlerBuilder MapDelete(WebApplication app, Delegate handler) => MapDelete(app, "", handler);

public RouteHandlerBuilder MapDelete(WebApplication app, string pattern, Delegate handler)
{
return app.MapDelete(BuildRoutePattern(pattern), handler)
.WithDefaults(this);
}

private string BuildRoutePattern(string pattern)
{
if (!pattern.StartsWith("/"))
{
pattern = $"{BaseRoute}{pattern}";
}

return pattern;
}
}
150 changes: 75 additions & 75 deletions src/WebUI/Infrastructure/ApiExceptionFilter.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
using Ardalis.GuardClauses;
using CleanArchitecture.Application.Common.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CleanArchitecture.WebUI.Filters;

public class ApiExceptionFilter : IEndpointFilter
{
private readonly IDictionary<Type, Func<Exception, IResult>> _exceptionHandlers;

public ApiExceptionFilter()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Func<Exception, IResult>>
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(NotFoundException), HandleNotFoundException },
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
};
using CleanArchitecture.Application.Common.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CleanArchitecture.WebUI.Filters;

public class ApiExceptionFilter : IEndpointFilter
{
private readonly IDictionary<Type, Func<Exception, IResult>> _exceptionHandlers;

public ApiExceptionFilter()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Func<Exception, IResult>>
{
{ typeof(ValidationException), HandleValidationException },
{ typeof(NotFoundException), HandleNotFoundException },
{ typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
{ typeof(ForbiddenAccessException), HandleForbiddenAccessException },
};
}

public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
Expand All @@ -37,71 +37,71 @@ public ApiExceptionFilter()

throw;
}
}

private IResult? HandleException(Exception ex)
}

private IResult? HandleException(Exception ex)
{
var type = ex.GetType();

if (_exceptionHandlers.ContainsKey(type))
{
return _exceptionHandlers[type].Invoke(ex);

if (_exceptionHandlers.ContainsKey(type))
{
return _exceptionHandlers[type].Invoke(ex);
}

return null;
}

private IResult HandleValidationException(Exception ex)
{
var exception = (ValidationException)ex;

var details = new ValidationProblemDetails(exception.Errors)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};

return Results.BadRequest(details);
}

private IResult HandleNotFoundException(Exception ex)
{
var exception = (NotFoundException)ex;

var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};

return Results.NotFound(details);
}

private IResult HandleUnauthorizedAccessException(Exception ex)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status401Unauthorized,
Title = "Unauthorized",
Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
};
return null;
}

private IResult HandleValidationException(Exception ex)
{
var exception = (ValidationException)ex;

var details = new ValidationProblemDetails(exception.Errors)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};

return Results.BadRequest(details);
}

private IResult HandleNotFoundException(Exception ex)
{
var exception = (NotFoundException)ex;

var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};

return Results.NotFound(details);
}

private IResult HandleUnauthorizedAccessException(Exception ex)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status401Unauthorized,
Title = "Unauthorized",
Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
};

return Results.Json(
details,
statusCode: StatusCodes.Status401Unauthorized);
}

private IResult HandleForbiddenAccessException(Exception ex)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Title = "Forbidden",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
statusCode: StatusCodes.Status401Unauthorized);
}

private IResult HandleForbiddenAccessException(Exception ex)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status403Forbidden,
Title = "Forbidden",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
};

return Results.Json(
details,
statusCode: StatusCodes.Status403Forbidden);
statusCode: StatusCodes.Status403Forbidden);
}
}
}
16 changes: 16 additions & 0 deletions src/WebUI/OidcConfiguration/OidcConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
using Microsoft.AspNetCore.Mvc;

namespace CleanArchitecture.WebUI.OidcConfiguration;

public class OidcConfiguration : AbstractEndpoint
{
public override void Map(WebApplication app)
{
MapGet(app, "/_configuration/{clientId}", (IClientRequestParametersProvider clientRequestParametersProvider, HttpContext context, string clientId) =>
{
var parameters = clientRequestParametersProvider.GetClientParameters(context, clientId);
return Results.Ok(parameters);
}, withDefaults: false).ExcludeFromDescription();
}
}
7 changes: 7 additions & 0 deletions src/WebUI/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CleanArchitecture.Infrastructure.Persistence;
using CleanArchitecture.WebUI;

var builder = WebApplication.CreateBuilder(args);

Expand Down Expand Up @@ -38,6 +39,12 @@
settings.DocumentPath = "/api/specification.json";
});

app.UseRouting();

app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();

app.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
Expand Down
31 changes: 23 additions & 8 deletions src/WebUI/wwwroot/api/specification.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"x-generator": "NSwag v13.19.0.0 (NJsonSchema v10.9.0.0 (Newtonsoft.Json v10.0.0.0))",
"x-generator": "NSwag v13.17.0.0 (NJsonSchema v10.8.0.0 (Newtonsoft.Json v13.0.0.0))",
"openapi": "3.0.0",
"info": {
"title": "CleanArchitecture API",
Expand Down Expand Up @@ -198,7 +198,12 @@
}
}
}
}
},
"security": [
{
"JWT": []
}
]
}
},
"/api/TodoItems/{id}": {
Expand Down Expand Up @@ -297,7 +302,12 @@
"200": {
"description": ""
}
}
},
"security": [
{
"JWT": []
}
]
}
}
},
Expand Down Expand Up @@ -342,7 +352,7 @@
"priorityLevels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LookupDto"
"$ref": "#/components/schemas/PriorityLevelDto"
}
},
"lists": {
Expand All @@ -353,15 +363,15 @@
}
}
},
"LookupDto": {
"PriorityLevelDto": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"value": {
"type": "integer",
"format": "int32"
},
"title": {
"name": {
"type": "string",
"nullable": true
}
Expand Down Expand Up @@ -503,5 +513,10 @@
]
}
}
}
},
"security": [
{
"JWT": []
}
]
}

0 comments on commit 3847651

Please sign in to comment.