server cleanup

This commit is contained in:
hamid
2026-06-16 01:46:53 +03:30
parent 69bbd28bb0
commit 5b4c0d183f
205 changed files with 641 additions and 628 deletions
@@ -0,0 +1,39 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Baya.WebFramework.Attributes;
/// <summary>
/// Documents the 200 OK response type of endpoint as ApiResult
/// </summary>
/// <typeparam name="TResponse">API response type that will be in data JSON property</typeparam>
public class ProducesOkApiResponseType<TResponse>:ProducesResponseTypeAttribute
{
private ProducesOkApiResponseType(int statusCode) : base(statusCode)
{
}
public ProducesOkApiResponseType() : base(typeof(ApiResult<TResponse>), StatusCodes.Status200OK)
{
}
private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes)
{
}
}
public class ProducesOkApiResponseType:ProducesResponseTypeAttribute
{
private ProducesOkApiResponseType(int statusCode) : base(statusCode)
{
}
public ProducesOkApiResponseType() : base(typeof(ApiResult), StatusCodes.Status200OK)
{
}
private ProducesOkApiResponseType(Type type, int statusCode, string contentType, params string[] additionalContentTypes) : base(type, statusCode, contentType, additionalContentTypes)
{
}
}
@@ -0,0 +1,52 @@
using System.Security.Claims;
using Baya.Application.Models.Common;
using Baya.SharedKernel.Extensions;
using Baya.WebFramework.Filters;
using Microsoft.AspNetCore.Mvc;
namespace Baya.WebFramework.BaseController;
public class BaseController : ControllerBase
{
protected string UserName => User.Identity?.Name;
protected int UserId => int.Parse(User.Identity.GetUserId());
protected string UserEmail => User.Identity.FindFirstValue(ClaimTypes.Email);
protected string UserRole => User.Identity.FindFirstValue(ClaimTypes.Role);
protected string UserKey => User.FindFirstValue(ClaimTypes.UserData);
protected IActionResult OperationResult<TModel>(OperationResult<TModel> result)
{
if (result is null)
return new ServerErrorResult("Server Error");
if (result.IsSuccess)
return result.Result is bool ? Ok() : Ok(result.Result);
if (result.IsNotFound)
{
AddErrors(result);
var notFoundErrors = new ValidationProblemDetails(ModelState);
return NotFound(notFoundErrors.Errors);
}
AddErrors(result);
var badRequestErrors = new ValidationProblemDetails(ModelState);
return BadRequest(badRequestErrors.Errors);
}
private void AddErrors<TModel>(OperationResult<TModel> result)
{
foreach (var error in result.ErrorMessages)
{
ModelState.AddModelError(error.Key,error.Value);
}
}
}
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSwag.AspNetCore" />
<PackageReference Include="NuGet.Packaging" />
<PackageReference Include="Asp.Versioning.Http" />
<PackageReference Include="Asp.Versioning.Mvc" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\Baya.Application\Baya.Application.csproj" />
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.CrossCutting\Baya.Infrastructure.CrossCutting.csproj" />
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.Identity\Baya.Infrastructure.Identity.csproj" />
<ProjectReference Include="..\..\Infrastructure\Baya.Infrastructure.Persistence\Baya.Infrastructure.Persistence.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,28 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace Baya.WebFramework.EndpointFilters;
public class BadRequestResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result = await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if (statusCodeResult.StatusCode != StatusCodes.Status400BadRequest)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.BadRequest(new ApiResult<object>(false, ApiResultStatusCode.BadRequest, valueHttp.Value));
}
return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.BadRequest));
}
}
@@ -0,0 +1,46 @@
using FluentValidation;
using Microsoft.AspNetCore.Http;
namespace Baya.WebFramework.EndpointFilters;
public class ModelStateValidationEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var validationSummery = new Dictionary<string, List<string>>();
foreach (var contextArgument in context.Arguments)
{
if (contextArgument is null)
continue;
var validator =
context.HttpContext.RequestServices.GetService(
typeof(IValidator<>).MakeGenericType(contextArgument.GetType())) as IValidator;
if (validator is null)
continue;
var validationResult = await validator.ValidateAsync(new ValidationContext<object>(contextArgument));
if (validationResult.IsValid) continue;
foreach (var validationResultError in validationResult.Errors)
{
if (validationSummery.TryGetValue(validationResultError.PropertyName, out var value))
{
value.Add(validationResultError.ErrorMessage);
continue;
}
validationSummery.Add(validationResultError.PropertyName, new (){validationResultError.ErrorMessage});
}
}
if (validationSummery.Count == 0)
return await next(context);
return Results.BadRequest(validationSummery);
}
}
@@ -0,0 +1,28 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace Baya.WebFramework.EndpointFilters;
public class NotFoundResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result = await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if (statusCodeResult.StatusCode != StatusCodes.Status404NotFound)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.BadRequest(new ApiResult<object>(false, ApiResultStatusCode.NotFound, valueHttp.Value));
}
return Results.BadRequest(new ApiResult(false, ApiResultStatusCode.NotFound));
}
}
@@ -0,0 +1,28 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
namespace Baya.WebFramework.EndpointFilters;
public class OkResultEndpointFilter:IEndpointFilter
{
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var result=await next(context);
if (result is not IStatusCodeHttpResult statusCodeResult)
{
return result;
}
if(statusCodeResult.StatusCode !=StatusCodes.Status200OK)
return result;
if (result is IValueHttpResult valueHttp)
{
return Results.Ok(new ApiResult<object>(true, ApiResultStatusCode.Success, valueHttp.Value));
}
return Results.Ok(new ApiResult(true, ApiResultStatusCode.Success));
}
}
@@ -0,0 +1,65 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Baya.WebFramework.Filters;
[Obsolete(message:"Separated filters added")]
public class ApiResultFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.Result is OkObjectResult okObjectResult)
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, okObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode };
}
else if (context.Result is OkResult okResult)
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success);
context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode };
}
else if (context.Result is BadRequestResult badRequestResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestResult.StatusCode };
context.HttpContext.Response.StatusCode = 400;
}
else if (context.Result is BadRequestObjectResult badRequestObjectResult)
{
var message = badRequestObjectResult.Value.ToString();
if (badRequestObjectResult.Value is SerializableError errors)
{
var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct();
message = string.Join(" | ", errorMessages);
}
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest, message);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = 400;
}
else if (context.Result is ContentResult contentResult)
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content);
context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode };
}
else if (context.Result is NotFoundResultAttribute notFoundResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound);
context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status404NotFound };
}
else if (context.Result is NotFoundObjectResult notFoundObjectResult)
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode };
}
else if (context.Result is ObjectResult objectResult && objectResult.StatusCode == null
&& !(objectResult.Value is ApiResult))
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, objectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = objectResult.StatusCode };
}
base.OnResultExecuting(context);
}
}
@@ -0,0 +1,37 @@
using Baya.Application.Models.ApiResult;
using Baya.SharedKernel.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Baya.WebFramework.Filters;
public class BadRequestResultFilterAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is BadRequestObjectResult badRequestObjectResult)) return;
var modelState = context.ModelState;
if (!modelState.IsValid)
{
var errors = new ValidationProblemDetails(modelState);
var message = ApiResultStatusCode.BadRequest.ToDisplay();
var apiResult = new ApiResult<IDictionary<string, string[]>>(false, ApiResultStatusCode.BadRequest, errors.Errors, message);
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
else
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.BadRequest,badRequestObjectResult.Value,ApiResultStatusCode.BadRequest.ToDisplay());
context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
}
@@ -0,0 +1,15 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Baya.WebFramework.Filters;
public class ContentResultFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is ContentResult contentResult)) return;
var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content);
context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode };
}
}
@@ -0,0 +1,62 @@
using Baya.Application.Models.ApiResult;
using Baya.SharedKernel.Extensions;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using StatusCodes = Microsoft.AspNetCore.Http.StatusCodes;
namespace Baya.WebFramework.Filters;
public class ModelStateValidationAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var contextActionArgument in context.ActionArguments.Values)
{
var viewModelValidator =
context.HttpContext.RequestServices.GetService(
typeof(IValidator<>).MakeGenericType(contextActionArgument.GetType()));
if (viewModelValidator is IValidator validator)
{
var validationResult =await validator.ValidateAsync(new ValidationContext<object>(contextActionArgument));
if (!validationResult.IsValid)
{
foreach (var validationResultError in validationResult.Errors)
{
context.ModelState.AddModelError(validationResultError.PropertyName, validationResultError.ErrorMessage);
}
}
}
}
var modelState = context.ModelState;
if (!modelState.IsValid)
{
var model = context.ActionArguments.FirstOrDefault().Value;
if (model != null)
{
var errors = new ValidationProblemDetails(modelState);
var message = ApiResultStatusCode.BadRequest.ToDisplay();
var apiResult = new ApiResult<IDictionary<string, string[]>>(false, ApiResultStatusCode.BadRequest, errors.Errors, message);
context.Result = new JsonResult(apiResult) { StatusCode = StatusCodes.Status400BadRequest };
context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
else
{
var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest);
context.Result = new JsonResult(apiResult) { StatusCode = 400 };
context.HttpContext.Response.StatusCode = 400;
}
}
await base.OnActionExecutionAsync(context, next);
}
}
@@ -0,0 +1,24 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Baya.WebFramework.Filters;
public class NotFoundResultAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if ((context.Result is NotFoundObjectResult notFoundObjectResult))
{
var apiResult = new ApiResult<object>(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode };
}
else if(context.Result is NotFoundResult)
{
var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound);
context.Result = new JsonResult(apiResult) { StatusCode =StatusCodes.Status404NotFound };
}
}
}
@@ -0,0 +1,28 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Baya.WebFramework.Filters;
public class OkResultAttribute:ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
switch (context.Result)
{
case OkObjectResult okObjectResult:
{
var apiResult = new ApiResult<object>(true, ApiResultStatusCode.Success, okObjectResult.Value);
context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode };
break;
}
case OkResult okResult:
{
var apiResult = new ApiResult(true, ApiResultStatusCode.Success);
context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode };
break;
}
default:return;
}
}
}
@@ -0,0 +1,22 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Baya.WebFramework.Filters;
public class ServerErrorResult:IActionResult
{
public string Message { get;}
public ServerErrorResult(string message)
{
Message = message;
}
public async Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = new ApiResult(false, ApiResultStatusCode.ServerError, Message);
await context.HttpContext.Response.WriteAsJsonAsync(response);
}
}
@@ -0,0 +1,63 @@
using Baya.Application.Models.ApiResult;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Baya.SharedKernel.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Baya.WebFramework.Middlewares;
public class ExceptionHandler(ILogger<ExceptionHandler> logger,IWebHostEnvironment environment) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken)
{
if (environment.IsDevelopment())
return false;
if (exception is FluentValidation.ValidationException validationException)
{
context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
var errors = new Dictionary<string, List<string>>();
foreach (var validationExceptionError in validationException.Errors)
{
if (!errors.ContainsKey(validationExceptionError.PropertyName))
errors.Add(validationExceptionError.PropertyName, new List<string>() { validationExceptionError.ErrorMessage });
else
errors[validationExceptionError.PropertyName].Add(validationExceptionError.ErrorMessage);
}
var apiResult = new ApiResult<IDictionary<string, List<string>>>(false, ApiResultStatusCode.EntityProcessError, errors, ApiResultStatusCode.EntityProcessError.ToDisplay());
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(apiResult, cancellationToken: cancellationToken);
return true;
}
var exceptionFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionFeature is not null)
logger.LogError(exceptionFeature.Error,
"Unhandled exception occured. Path: {exceptionUrlPath} ."
, exceptionFeature.Path
);
else
logger.LogError(exception, "Error captured in global exception handler");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";
var response = new ApiResult(false,
ApiResultStatusCode.ServerError, "Internal Server Error");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(response, cancellationToken: cancellationToken);
return true;
}
}
@@ -0,0 +1,45 @@
using Asp.Versioning;
using Microsoft.Extensions.DependencyInjection;
namespace Baya.WebFramework.ServiceConfiguration;
public static class ServiceCollectionExtension
{
public static IServiceCollection AddWebFrameworkServices(this IServiceCollection services)
{
services.AddApiVersioning(options =>
{
//url segment => {version}
options.AssumeDefaultVersionWhenUnspecified = true; //default => false;
options.DefaultApiVersion = new ApiVersion(1, 0); //v1.0 == v1
options.ReportApiVersions = true;
//ApiVersion.TryParse("1.0", out var version10);
//ApiVersion.TryParse("1", out var version1);
//var a = version10 == version1;
//options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
// api/posts?api-version=1
//options.ApiVersionReader = new UrlSegmentApiVersionReader();
// api/v1/posts
//options.ApiVersionReader = new HeaderApiVersionReader(new[] { "Api-Version" });
// header => Api-Version : 1
//options.ApiVersionReader = new MediaTypeApiVersionReader()
//options.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"), new UrlSegmentApiVersionReader())
// combine of [querystring] & [urlsegment]
}).AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'V";
options.SubstituteApiVersionInUrl = true;
});;
return services;
}
}
@@ -0,0 +1,25 @@
using Baya.SharedKernel.Extensions;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace Baya.WebFramework.Swagger;
public class ApiVersionDocumentProcessor: IDocumentProcessor
{
public void Process(DocumentProcessorContext context)
{
// Filter out operations that do not match the current document version
var version = context.Document.Info.Version; // e.g., "v1"
var pathsToRemove = context.Document.Paths
.Where(pathItem => !RegExHelpers.MatchesApiVersion( version,pathItem.Key))
.Select(path => path.Key)
.ToList();
foreach (var path in pathsToRemove)
{
context.Document.Paths.Remove(path);
}
}
}
@@ -0,0 +1,122 @@
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
using Pluralize.NET;
namespace Baya.WebFramework.Swagger;
public class ApplySummariesOperationFilter : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
if (context.ControllerType is null)
return true;
var actionName = context.MethodInfo.Name;
var controllerName = context.ControllerType.Name.Replace("Controller", "");
var pluralizer = new Pluralizer();
var singularizeName =pluralizer.Singularize(controllerName);
var pluralizeName = pluralizer.Pluralize(singularizeName);
var parameterCount = context.OperationDescription.Operation.Parameters
.Count(p => p.Name != "version" && p.Name != "api-version");
if (IsGetAllAction(actionName, pluralizeName, singularizeName, parameterCount))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Returns all {pluralizeName}";
}
}
else if (IsActionName(actionName, singularizeName, "Post", "Create"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Creates a {singularizeName}";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation";
}
}
else if (IsActionName(actionName, singularizeName, "Read", "Get"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Retrieves a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}";
}
}
else if (IsActionName(actionName, singularizeName, "Put", "Edit", "Update"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Updates a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A {singularizeName} representation";
}
}
else if (IsActionName(actionName, singularizeName, "Delete", "Remove"))
{
if (string.IsNullOrEmpty(context.OperationDescription.Operation.Summary))
{
context.OperationDescription.Operation.Summary = $"Deletes a {singularizeName} by unique id";
}
if (context.OperationDescription.Operation.Parameters.Count > 0 &&
string.IsNullOrEmpty(context.OperationDescription.Operation.Parameters[0].Description))
{
context.OperationDescription.Operation.Parameters[0].Description = $"A unique id for the {singularizeName}";
}
}
return true;
}
private bool IsActionName(string actionName, string singularizeName, params string[] names)
{
foreach (var name in names)
{
if (actionName.Equals(name, StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}ById", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{singularizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{singularizeName}ById", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private bool IsGetAllAction(string actionName, string pluralizeName, string singularizeName, int parameterCount)
{
var actionNames = new[] { "Get", "Read", "Select" };
foreach (var name in actionNames)
{
if ((actionName.Equals(name, StringComparison.OrdinalIgnoreCase) && parameterCount == 0) ||
actionName.Equals($"{name}All", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}{pluralizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}All{singularizeName}", StringComparison.OrdinalIgnoreCase) ||
actionName.Equals($"{name}All{pluralizeName}", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
@@ -0,0 +1,36 @@
using NSwag;
using NSwag.Generation.Processors;
using NSwag.Generation.Processors.Contexts;
namespace Baya.WebFramework.Swagger;
public class CustomTokenRequiredOperationFilter : IOperationProcessor
{
public bool Process(OperationProcessorContext context)
{
var hasAttribute = context.MethodInfo
.GetCustomAttributes(typeof(RequireTokenWithoutAuthorizationAttribute), false).Any();
if (hasAttribute)
{
// Add security requirements to the operation
var securityScheme = new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = OpenApiSecurityApiKeyLocation.Header,
Name = "Authorization"
};
var securityRequirement = new OpenApiSecurityRequirement { { securityScheme.Scheme, new List<string>() } };
context.OperationDescription.Operation.Security=[securityRequirement];
}
return true;
}
}
@@ -0,0 +1,11 @@
namespace Baya.WebFramework.Swagger;
/// <summary>
/// Marker Attribute for Custom Actions or controllers that need token but without authorization check
/// </summary>
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Method)]
public class RequireTokenWithoutAuthorizationAttribute : Attribute
{
};
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NSwag;
namespace Baya.WebFramework.Swagger;
public static class SwaggerConfigurationExtensions
{
public static void AddSwagger(this IServiceCollection services,
params string[] versions)
{
ArgumentNullException.ThrowIfNull(services, nameof(services));
foreach (var version in versions)
{
services.AddOpenApiDocument(options =>
{
options.Title = "Clean Architecture OpenAPI docs";
options.Version = version;
options.DocumentName = version;
options.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme()
{
Description = "Enter JWT Token ONLY",
In = OpenApiSecurityApiKeyLocation.Header,
Name = "Authorization",
Type = OpenApiSecuritySchemeType.Http,
Scheme = JwtBearerDefaults.AuthenticationScheme,
});
options.DocumentProcessors.Add(new ApiVersionDocumentProcessor());
options.OperationProcessors.Add(new ApplySummariesOperationFilter());
options.OperationProcessors.Add(new CustomTokenRequiredOperationFilter());
options.OperationProcessors.Add(
new NSwag.Generation.Processors.Security.AspNetCoreOperationSecurityScopeProcessor("Bearer"));
});
}
}
public static void UseSwaggerAndUi(this WebApplication app)
{
ArgumentNullException.ThrowIfNull(app, nameof(app));
app.UseOpenApi();
app.UseSwaggerUi(options =>
{
options.PersistAuthorization = true;
options.EnableTryItOut = true;
options.Path = "/swagger";
});
app.UseReDoc(settings =>
{
settings.Path = "/api-docs/{documentName}";
settings.DocumentTitle = "Clean Architecture API doc sample";
});
}
}