├── .gitignore └── how_to_structure_mini_api ├── Api ├── Data │ ├── Post.cs │ ├── Blog.cs │ └── AppDbContext.cs ├── appsettings.Development.json ├── appsettings.json ├── Framework │ ├── Handler.cs │ ├── RouteValueExtensions.cs │ ├── Request.cs │ └── WebApplicationExtensions.cs ├── EndpointAuthenticationDeclaration.cs ├── Services │ └── Blogs │ │ ├── GetBlogs.cs │ │ ├── Test.cs │ │ ├── CreateBlog.cs │ │ └── GetBlog.cs ├── Api.csproj ├── Properties │ └── launchSettings.json ├── Program.cs └── RegisterServices.cs ├── MVC ├── appsettings.Development.json ├── Program.cs ├── appsettings.json ├── MVC.csproj ├── Controllers │ └── TestController.cs └── Properties │ └── launchSettings.json ├── aspnetcore-minimal-api.sln └── performance_test.linq /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .vs 4 | .idea 5 | .vscode -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Data/Post.cs: -------------------------------------------------------------------------------- 1 | public class Post 2 | { 3 | public int Id { get; set; } 4 | public string Title { get; set; } 5 | public string Body { get; set; } 6 | } -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | builder.Services.AddControllers(); 4 | 5 | var app = builder.Build(); 6 | 7 | app.MapControllers(); 8 | 9 | app.Run(); -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Data/Blog.cs: -------------------------------------------------------------------------------- 1 | public class Blog 2 | { 3 | public int Id { get; set; } 4 | public string Title { get; set; } 5 | public string CreatedBy { get; set; } 6 | 7 | public List Posts { get; set; } = new(); 8 | } 9 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Data/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | public class AppDbContext : DbContext 4 | { 5 | public AppDbContext(DbContextOptions options) 6 | : base(options) 7 | { 8 | 9 | } 10 | 11 | public DbSet Blogs { get; set; } 12 | public DbSet Posts { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/MVC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Framework/Handler.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Api.Framework 4 | { 5 | public interface IHandler 6 | { 7 | public Task RunAsync(object v); 8 | } 9 | 10 | public abstract class Handler : IHandler 11 | where TIn : IRequest 12 | { 13 | public abstract Task Run(TIn v); 14 | 15 | public async Task RunAsync(object v) 16 | { 17 | var result = await Run((TIn)v); 18 | return result; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace MVC.Controllers 4 | { 5 | public class TestController : ControllerBase 6 | { 7 | [HttpPost("/test/{id}")] 8 | public object GetBlog(int id, [FromQuery] string v, [FromBody] TestRequest request) 9 | { 10 | return new 11 | { 12 | result = $"{request.Title}_{id}_{v}" 13 | }; 14 | } 15 | 16 | public class TestRequest 17 | { 18 | public string Title { get; set; } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/EndpointAuthenticationDeclaration.cs: -------------------------------------------------------------------------------- 1 | namespace Api 2 | { 3 | public class EndpointAuthenticationDeclaration 4 | { 5 | public static void Anonymous(params IEndpointConventionBuilder[] ecb) 6 | { 7 | foreach (var e in ecb) 8 | { 9 | e.AllowAnonymous(); 10 | } 11 | } 12 | 13 | public static void Admin(params IEndpointConventionBuilder[] ecb) 14 | { 15 | foreach (var e in ecb) 16 | { 17 | e.RequireAuthorization("admin"); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Services/Blogs/GetBlogs.cs: -------------------------------------------------------------------------------- 1 | using Api.Framework; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Api.Services 5 | { 6 | public class GetBlogsRequest : IRequest> 7 | { 8 | } 9 | 10 | public class GetBlogs : Handler> 11 | { 12 | private readonly AppDbContext ctx; 13 | 14 | public GetBlogs(AppDbContext ctx) 15 | { 16 | this.ctx = ctx; 17 | } 18 | 19 | public override Task> Run(GetBlogsRequest v) 20 | { 21 | return ctx.Blogs.ToListAsync(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Framework/RouteValueExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Api.Framework 2 | { 3 | public static class RouteValueExtensions 4 | { 5 | public static int GetInt(this RouteValueDictionary routeValues, string key) 6 | { 7 | if (routeValues.TryGetValue(key, out var objValue)) 8 | { 9 | if (objValue is string strValue) 10 | { 11 | if (int.TryParse(strValue, out var number)) 12 | { 13 | return number; 14 | } 15 | } 16 | } 17 | 18 | return default; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Framework/Request.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Api.Framework 4 | { 5 | public interface IRequest 6 | { 7 | } 8 | 9 | public interface IFromQuery 10 | { 11 | void BindFromQuery(IQueryCollection queryCollection); 12 | } 13 | public interface IFromRoute 14 | { 15 | void BindFromRoute(RouteValueDictionary routeValues); 16 | } 17 | public interface IWithUserContext 18 | { 19 | void BindFromUser(ClaimsPrincipal user); 20 | } 21 | 22 | public interface IFromJsonBody 23 | { 24 | } 25 | 26 | public interface IRequest : IRequest 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:6843", 7 | "sslPort": 44335 8 | } 9 | }, 10 | "profiles": { 11 | "Api": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:5000", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Api; 2 | using Api.Framework; 3 | using Api.Services; 4 | using static Api.EndpointAuthenticationDeclaration; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | builder.Services.AddApiServices(); 8 | 9 | var app = builder.Build(); 10 | 11 | app.UseAuthentication(); 12 | app.UseAuthorization(); 13 | 14 | Anonymous( 15 | app.MapGet("/blogs"), 16 | app.MapGet("/blogs/{id}"), 17 | // curl -i -X POST -H "Content-Type: application/json" -d "{\"title\":\"test body\"}" "http://localhost:5000/test/1?v=test" 18 | app.MapPost("/test/{id}") 19 | ); 20 | 21 | Admin( 22 | // curl -i -X POST -H "Content-Type: application/json" -d "{\"title\":\"boi\"}" http://localhost:5000/admin/blogs 23 | app.MapPost("/admin/blogs") 24 | ); 25 | 26 | app.Run(); -------------------------------------------------------------------------------- /how_to_structure_mini_api/MVC/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:59498", 8 | "sslPort": 44389 9 | } 10 | }, 11 | "profiles": { 12 | "MVC": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "applicationUrl": "http://localhost:5001", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Services/Blogs/Test.cs: -------------------------------------------------------------------------------- 1 | using Api.Framework; 2 | 3 | namespace Api.Services 4 | { 5 | public class TestRequest : IRequest, IFromJsonBody, IFromRoute, IFromQuery 6 | { 7 | public string Title { get; set; } 8 | public int FromRoute { get; set; } 9 | public string FromQuery { get; set; } 10 | 11 | public void BindFromQuery(IQueryCollection queryCollection) 12 | { 13 | if(queryCollection.TryGetValue("v", out var v)) 14 | { 15 | FromQuery = v; 16 | } 17 | } 18 | 19 | public void BindFromRoute(RouteValueDictionary routeValues) 20 | { 21 | FromRoute = routeValues.GetInt("id"); 22 | } 23 | } 24 | 25 | public class Test : Handler 26 | { 27 | public override Task Run(TestRequest v) 28 | { 29 | return Task.FromResult((object) new 30 | { 31 | result = $"{v.Title}_{v.FromRoute}_{v.FromQuery}" 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Services/Blogs/CreateBlog.cs: -------------------------------------------------------------------------------- 1 | using Api.Framework; 2 | using System.Security.Claims; 3 | 4 | namespace Api.Services 5 | { 6 | public class CreateBlogRequest : IRequest, IFromJsonBody, IWithUserContext 7 | { 8 | public string Title { get; set; } 9 | public string CreatedBy { get; set; } 10 | 11 | public void BindFromUser(ClaimsPrincipal user) 12 | { 13 | CreatedBy = user.Claims.FirstOrDefault(x => x.Type == "username").Value; 14 | } 15 | } 16 | 17 | public class CreateBlog : Handler 18 | { 19 | private readonly AppDbContext ctx; 20 | 21 | public CreateBlog(AppDbContext ctx) 22 | { 23 | this.ctx = ctx; 24 | } 25 | 26 | public override async Task Run(CreateBlogRequest v) 27 | { 28 | var blog = new Blog 29 | { 30 | Title = v.Title, 31 | CreatedBy = v.CreatedBy 32 | }; 33 | 34 | ctx.Add(blog); 35 | 36 | await ctx.SaveChangesAsync(); 37 | 38 | return blog; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Services/Blogs/GetBlog.cs: -------------------------------------------------------------------------------- 1 | using Api.Framework; 2 | using FluentValidation; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Security.Claims; 5 | 6 | namespace Api.Services 7 | { 8 | public class GetBlogRequest : IRequest, IFromRoute 9 | { 10 | public int Id { get; private set; } 11 | 12 | public void BindFromRoute(RouteValueDictionary routeValues) 13 | { 14 | Id = routeValues.GetInt("id"); 15 | } 16 | } 17 | 18 | public class GetBlogRequestValidation : AbstractValidator 19 | { 20 | public GetBlogRequestValidation() 21 | { 22 | RuleFor(r => r.Id).Must(v => v > 0).WithMessage("Id needs to be more than 0."); 23 | } 24 | } 25 | 26 | public class GetBlog : Handler 27 | { 28 | private readonly AppDbContext ctx; 29 | 30 | public GetBlog(AppDbContext ctx) 31 | { 32 | this.ctx = ctx; 33 | } 34 | 35 | public override Task Run(GetBlogRequest v) 36 | { 37 | return ctx.Blogs.FirstOrDefaultAsync(x => x.Id == v.Id); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/aspnetcore-minimal-api.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31710.8 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api", "Api\Api.csproj", "{A63A54F1-8C02-4D58-B805-FCF93E9E08C5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MVC", "MVC\MVC.csproj", "{D32BF57E-E5B9-4776-9B34-87F77BC791ED}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A63A54F1-8C02-4D58-B805-FCF93E9E08C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {A63A54F1-8C02-4D58-B805-FCF93E9E08C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {A63A54F1-8C02-4D58-B805-FCF93E9E08C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {A63A54F1-8C02-4D58-B805-FCF93E9E08C5}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {D32BF57E-E5B9-4776-9B34-87F77BC791ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {D32BF57E-E5B9-4776-9B34-87F77BC791ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {D32BF57E-E5B9-4776-9B34-87F77BC791ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {D32BF57E-E5B9-4776-9B34-87F77BC791ED}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {BBB74881-561B-425E-AD0E-4D96A44BFAD2} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/performance_test.linq: -------------------------------------------------------------------------------- 1 | 2 | NBomber 3 | NBomber.Http 4 | NBomber.Plugins.Http.CSharp 5 | NBomber.CSharp 6 | FsToolkit.ErrorHandling 7 | NBomber.Contracts 8 | System.Net.Http.Json 9 | 10 | 11 | void Main() 12 | { 13 | var factory = HttpClientFactory.Create(); 14 | var content = JsonContent.Create(new {title = "test title"}); 15 | 16 | var mvc = Step.Create("mvc", factory, async context => 17 | { 18 | var response = await context.Client.PostAsync("http://localhost:5001/test/1?v=test", content); 19 | 20 | return response.IsSuccessStatusCode 21 | ? Response.Ok(statusCode: (int) response.StatusCode) 22 | : Response.Fail(statusCode: (int) response.StatusCode); 23 | }); 24 | 25 | var minApi = Step.Create("min_api", factory, async context => 26 | { 27 | var response = await context.Client.PostAsync("http://localhost:5000/test/1?v=test", content); 28 | 29 | return response.IsSuccessStatusCode 30 | ? Response.Ok(statusCode: (int)response.StatusCode) 31 | : Response.Fail(statusCode: (int)response.StatusCode); 32 | }); 33 | 34 | var mvc_scenario = ScenarioBuilder 35 | .CreateScenario("mvc_scenario", mvc) 36 | .WithWarmUpDuration(TimeSpan.FromSeconds(10)) 37 | .WithLoadSimulations(Simulation.KeepConstant(24, TimeSpan.FromSeconds(60))); 38 | 39 | var minApiScenario = ScenarioBuilder 40 | .CreateScenario("min_api_scenario", minApi) 41 | .WithWarmUpDuration(TimeSpan.FromSeconds(10)) 42 | .WithLoadSimulations(Simulation.KeepConstant(24, TimeSpan.FromSeconds(60))); 43 | 44 | NBomberRunner 45 | .RegisterScenarios(minApiScenario, mvc_scenario) 46 | .Run(); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/RegisterServices.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System.Security.Claims; 3 | using Api.Framework; 4 | using FluentValidation; 5 | 6 | namespace Api 7 | { 8 | public static class RegisterServices 9 | { 10 | public static void AddApiServices(this IServiceCollection services) 11 | { 12 | services.AddDbContext(o => o.UseInMemoryDatabase("myDb")); 13 | 14 | services.AddServices() 15 | .AddValidation(); 16 | 17 | services.AddAuthentication("bearer") 18 | .AddJwtBearer("bearer", opt => 19 | { 20 | opt.Events = new(); 21 | opt.Events.OnMessageReceived = (ctx) => 22 | { 23 | var claims = new Claim[] { new("username", "bob master 3000") }; 24 | var identity = new ClaimsIdentity(claims, "bearer"); 25 | ctx.Principal = new ClaimsPrincipal(identity); 26 | ctx.Success(); 27 | return Task.CompletedTask; 28 | }; 29 | }); 30 | 31 | services.AddAuthorization(config => 32 | { 33 | config.AddPolicy("admin", pb => pb 34 | .RequireAuthenticatedUser() 35 | .AddAuthenticationSchemes("bearer")); 36 | }); 37 | } 38 | 39 | public static IServiceCollection AddServices(this IServiceCollection services) 40 | { 41 | var targetServices = typeof(T).Assembly.GetTypes() 42 | .Select(t => (t, t.BaseType)) 43 | .Where((tuple) => tuple.BaseType != null) 44 | .Where((tuple) => tuple.BaseType.IsGenericType && tuple.BaseType.GetGenericTypeDefinition().IsEquivalentTo(typeof(Handler<,>))); 45 | 46 | foreach (var s in targetServices) 47 | { 48 | services.AddTransient(s.Item2, s.Item1); 49 | } 50 | 51 | return services; 52 | } 53 | 54 | public static IServiceCollection AddValidation(this IServiceCollection services) 55 | { 56 | var validators = typeof(T).Assembly.GetTypes() 57 | .Select(t => (t, t.BaseType)) 58 | .Where((tuple) => tuple.BaseType != null) 59 | .Where((tuple) => tuple.BaseType.IsGenericType 60 | && tuple.BaseType.IsAbstract 61 | && tuple.BaseType.GetGenericTypeDefinition().IsEquivalentTo(typeof(AbstractValidator<>))) 62 | .Select((tuple) => (tuple.t, tuple.BaseType.GetGenericArguments()[0])); 63 | 64 | foreach (var v in validators) 65 | { 66 | var validorInterfaceType = typeof(IValidator<>).MakeGenericType(v.Item2); 67 | services.AddTransient(validorInterfaceType, v.Item1); 68 | } 69 | 70 | return services; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /how_to_structure_mini_api/Api/Framework/WebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Api.Framework; 2 | using FluentValidation; 3 | 4 | namespace Api.Framework 5 | { 6 | public static class WebApplicationExtensions 7 | { 8 | public static IEndpointConventionBuilder MapGet( 9 | this WebApplication app, 10 | string pattern 11 | ) 12 | where TRequest : IRequest, new() 13 | { 14 | return app.MapGet(pattern, RequestHandler); 15 | } 16 | 17 | public static IEndpointConventionBuilder MapPost( 18 | this WebApplication app, 19 | string pattern 20 | ) 21 | where TRequest : IRequest, new() 22 | { 23 | return app.MapPost(pattern, RequestHandler); 24 | } 25 | 26 | private static async Task RequestHandler(HttpContext ctx) 27 | where TRequest : IRequest, new() 28 | { 29 | var request = await ctx.ModelBindAsync(); 30 | 31 | if (!await ctx.ValidateAsync(request)) 32 | { 33 | return; 34 | } 35 | 36 | await ctx.HandleAsync(request); 37 | } 38 | 39 | private static async Task ModelBindAsync(this HttpContext ctx) 40 | where TRequest : IRequest, new() 41 | { 42 | var requestType = typeof(TRequest); 43 | var interfaces = requestType.GetInterfaces(); 44 | 45 | TRequest result = interfaces.Any(x => x.Equals(typeof(IFromJsonBody))) 46 | ? (TRequest)await ctx.Request.ReadFromJsonAsync(requestType) 47 | : new TRequest(); 48 | 49 | if(result is IFromRoute fr) 50 | { 51 | fr.BindFromRoute(ctx.Request.RouteValues); 52 | } 53 | 54 | if(result is IFromQuery fq) 55 | { 56 | fq.BindFromQuery(ctx.Request.Query); 57 | } 58 | 59 | if (result is IWithUserContext wu) 60 | { 61 | wu.BindFromUser(ctx.User); 62 | } 63 | 64 | return result; 65 | } 66 | 67 | private static async Task ValidateAsync(this HttpContext ctx, TRequest request) 68 | where TRequest : IRequest 69 | { 70 | var validorInterfaceType = typeof(IValidator<>).MakeGenericType(typeof(TRequest)); 71 | var validator = (IValidator)ctx.RequestServices.GetService(validorInterfaceType); 72 | if (validator != null) 73 | { 74 | var context = new ValidationContext(request); 75 | var validationResult = validator.Validate(context); 76 | if (!validationResult.IsValid) 77 | { 78 | var validationErrors = new Dictionary(); 79 | 80 | foreach (var error in validationResult.Errors) 81 | { 82 | validationErrors[error.PropertyName] = error.ErrorMessage; 83 | } 84 | 85 | ctx.Response.StatusCode = 400; 86 | await ctx.Response.WriteAsJsonAsync(new 87 | { 88 | message = "failed validation.", 89 | errors = validationErrors 90 | }); 91 | return false; 92 | } 93 | } 94 | 95 | return true; 96 | } 97 | 98 | private static async Task HandleAsync(this HttpContext ctx, TRequest request) 99 | where TRequest : IRequest 100 | { 101 | var returnType = typeof(TRequest).GetInterfaces()[0].GetGenericArguments()[0]; 102 | 103 | var handlerType = typeof(Handler<,>).MakeGenericType(typeof(TRequest), returnType); 104 | 105 | var handler = (IHandler)ctx.RequestServices.GetService(handlerType); 106 | 107 | var result = await handler.RunAsync(request); 108 | await ctx.Response.WriteAsJsonAsync(result); 109 | } 110 | } 111 | } --------------------------------------------------------------------------------