-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Source generator for converting WebAPIService classes to MediatRed Mi…
…nimal API registrations
- Loading branch information
1 parent
31c0495
commit fad5c64
Showing
44 changed files
with
1,251 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# API Framework | ||
|
||
* status: proposed | ||
|
||
* date: 2023-09-13 | ||
* deciders: jezzsantos | ||
|
||
# Context and Problem Statement | ||
|
||
When it comes to writing REST web APIs in the template, we want to establish scalable patterns that focus on the API's requests and responses and the cross-cutting concerns involved: | ||
|
||
* Routes and verbs | ||
* Authenticated or not | ||
* Authorized or not, by roles with (RBAC) | ||
* Authorized or not, by feature set | ||
* Request Validation | ||
* Rate limiting | ||
* Response Caching | ||
* Response types (e.g. JSON or Stream) | ||
* Mapping etc | ||
* Exception Handling | ||
* etc... | ||
|
||
We know that in this architecture, the web API will mostly be delegating to an in-proc Application layer, and so we know that there will not be much code in this layer except to identify the Application Layer call. | ||
|
||
We also know that the requests and responses need to be easily referenced by clients and tests. | ||
|
||
Thus, we need a simple, structured, and consistent way to define request and response types and decorate them with various attributes. | ||
|
||
## Considered Options | ||
|
||
The traditional .NET options include: | ||
|
||
1. ASP.NET Minimal APIs ([MediatR](https://github.com/jbogard/MediatR)'ed) | ||
2. ASP.NET Minimal APIs (out of the box) | ||
3. ASP.NET Controllers | ||
4. Web frameworks like: ServiceStack | ||
|
||
## Decision Outcome | ||
|
||
`MediatR'ed Minimal APIs` | ||
|
||
- ASP.NET controllers were never a great abstraction to represent REST APIs. They were a poor adaptation of the ASP.NET MVC implementation re-purposed for sending JSON, and long overdue for a redesign. Until recently (< .net 6.0) MVC Controllers were the only choice from the ASP.NET team. | ||
- ASP.NET Minimal APIs (out of the box > .net 6.0) are simple and easy to define for demonstrating example code, but for use in larger systems are very awkward to organize, maintain, test, and reuse, as they are today. | ||
- ServiceStack is an ideal web framework for structuring and handling complex web APIs. It has resolved many of the design challenges that ASP.NET controllers has suffered from. It implements the [REPR design pattern](https://deviq.com/design-patterns/repr-design-pattern) very well indeed, and is a delight to use, in many aspects. However, the framework today has a huge surface area, which we are not interested in leveraging most of. It is not so well known to the wider developer community, in part because it is also licensed per developer for a significant annual fee. This last point disqualifies it for use in this template. | ||
- ASP.NET Minimal API's that are MediatR'ed and that can remove some of the redundancy and tedium of the current MediatR patterns (i.e. one ctor per handler, and duplicate ctor for api collections) can bring a more usable, structured, and testable way to define minimal APIs that is close to the same feel of defining and using ServiceStack API's, that does not change them functionally, but does offer more maintainable ways to define and reuse them across a whole system |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Web Framework | ||
|
||
## Design Drivers | ||
|
||
1. We want to leverage core supported Microsoft ASP.NET facilities, rather than some other bespoke framework (like ServiceStack.net). | ||
2. We are choosing Minimal API's over Controllers. | ||
3. We want to model Requests and Responses that are related, easy to validate and in one place the code, We desire the [REPR design pattern](https://deviq.com/design-patterns/repr-design-pattern). | ||
4. However, the standard Minimal API patterns are difficult to organize and maintain in larger codebases. | ||
5. We want a design that is easier to define and organize the API into modules, but yet have them be realised as Minimal APIs. | ||
6. We want to have automatic validation, as whole requests | ||
7. We want to [MediatR](https://github.com/jbogard/MediatR) handlers, to make Minimal API registration and dependency injection easier | ||
|
||
### Modularity | ||
|
||
One of the distinguishing design principles of a Modular Monolith over a Monolith is the ability to deploy any, all or some the API's in any number of deployment units. Taken to the extreme, you would end up with granular microservices. But smaller steps are very acceptable depending on the stage of the SaaS product. | ||
|
||
The ability to deploy any (Subdomain) of the code to a separate web host, should be quick and easy to accomplish. | ||
|
||
One of the things that has to be easy to do, is to register who the endpoints of a subdomain in whatever host you like, as well as all its dependencies. | ||
|
||
With minimal API's there should be a modular way of registering both its endpoints and handlers, and then moving them to other hosts later. | ||
|
||
### Organisation | ||
|
||
The design of Minimal API's makes developing 10s or 100s of them in a single project quite unwieldy. They certainly would not live in one file. | ||
|
||
Since they are registered as handlers, there is no concept of groups of API's. Whereas many API endpoints are naturally grouped or categorized. This is certainly the case when exposing subdomains. | ||
|
||
When using MediatR to register handlers for minimal API's, and with dependency injection, it becomes quite tedious and repetitive to write a handler class for every route, when many routes are grouped and will be sharing the same dependencies. | ||
|
||
There are better ways to organize these groups of endpoints into classes, and test them more easily. | ||
|
||
### Validation | ||
|
||
When you design endpoints you want the requests and responses to be coupled, and you want the requests to be validated automatically when requests come in. Writing wiring code for validation is lso very tedious and error prone, and so is writing code to response with errors in a consistent manner. | ||
|
||
We want the codebase to make validation easier to do, and apply it automatically and have standard ways to report errors detected by it. | ||
|
||
## Configuring API's | ||
|
||
All APIs will be defined in a separate project that is initially part of a subdomain group of code. That project can then be registered as a module into a specific web host, and with it all the endpoints, handlers, dependencies needed for all layers of the subdomain. | ||
|
||
The web host, will then code generate the endpoint declarations and handlers and register them with Minimal API, and other components can be registered with the IoC. | ||
|
||
### Reference the Source Generator | ||
|
||
Every API project must reference the Source Generators in `Infrastructure.WebApi.Generators`. | ||
|
||
EveryAPI must provide a plugin. | ||
|
||
The plugin will then automatically call the source-generated registration code, update the runtime configuration of the web host and populate the IoC automatically. | ||
|
||
The configuration of the web host and its features will be encapsulated and provided by various extension methods, so that all API hosts are consistent. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<component name="ProjectRunConfigurationManager"> | ||
<configuration default="false" name="AllHosts" type="CompoundRunConfigurationType"> | ||
<toRun name="ApiHost1: ApiHost1-Development" type="LaunchSettings" /> | ||
<method v="2" /> | ||
</configuration> | ||
</component> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net7.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\CarsApi\CarsApi.csproj" /> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Common\Infrastructure.WebApi.Common.csproj" /> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Interfaces\Infrastructure.WebApi.Interfaces.csproj" /> | ||
</ItemGroup> | ||
|
||
<!-- Runs the source generator (in memory) on build --> | ||
<ItemGroup> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Generators\Infrastructure.WebApi.Generators.csproj" | ||
OutputItemType="Analyzer" | ||
ReferenceOutputAssembly="false" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
using CarsApplication; | ||
using Infrastructure.WebApi.Common; | ||
#if TESTINGONLY | ||
using Infrastructure.WebApi.Interfaces.Operations.TestingOnly; | ||
#endif | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
|
||
builder.Services.AddMediatR(configuration => | ||
{ | ||
//TODO: we will have make sure that we have the assemblies of all APIs here somehow | ||
configuration.RegisterServicesFromAssembly(typeof(Program).Assembly); | ||
}); | ||
builder.Services.AddScoped<ICarsApplication, CarsApplication.CarsApplication>(); | ||
|
||
var app = builder.Build(); | ||
|
||
// app.MapGet("/cars/{id}", | ||
// ([AsParameters] GetCarRequest request) => | ||
// Results.Ok(new GetCarResponse { Message = $"Hello car {request.Id}!" })); | ||
|
||
//TODO: Need to build these registrations at startup by examining classes | ||
#if TESTINGONLY | ||
app.MediateGet<GetTestingOnlyRequest, GetTestingOnlyResponse>("/testingonly/{id}"); | ||
#endif | ||
|
||
//app.RegisterRoutes(); | ||
|
||
//TODO: need to combine with validation | ||
//TODO: need to add swaggerUI | ||
//TODO: need ot register modules | ||
|
||
app.Run(); | ||
|
||
|
||
public partial class Program | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
{ | ||
"profiles": { | ||
"ApiHost1-Development": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": false, | ||
"applicationUrl": "https://localhost:5001", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Development" | ||
} | ||
}, | ||
"ApiHost1-CI": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": false, | ||
"applicationUrl": "https://localhost:5001", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "CI" | ||
} | ||
}, | ||
"ApiHost1-Production": { | ||
"commandName": "Project", | ||
"dotnetRunMessages": true, | ||
"launchBrowser": false, | ||
"applicationUrl": "https://api.saastack.io", | ||
"environmentVariables": { | ||
"ASPNETCORE_ENVIRONMENT": "Production" | ||
} | ||
}, | ||
"SourceGenerator-Development": { | ||
"commandName": "DebugRoslynComponent", | ||
"targetProject": "../ApiHost1/ApiHost1.csproj" | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
#if TESTINGONLY | ||
using Infrastructure.WebApi.Interfaces; | ||
using Infrastructure.WebApi.Interfaces.Operations.TestingOnly; | ||
|
||
namespace ApiHost1.Services.TestingOnly; | ||
|
||
public class TestingOnlyApi : IWebApiService | ||
{ | ||
[WebApiRoute("/testingonly/{id}", WebApiOperation.Get)] | ||
public async Task<IResult> Get(GetTestingOnlyRequest request, CancellationToken cancellationToken) | ||
{ | ||
await Task.CompletedTask; | ||
return Results.Ok(new GetTestingOnlyResponse { Message = "amessage" }); | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"Logging": { | ||
"LogLevel": { | ||
"Default": "Information", | ||
"Microsoft.AspNetCore": "Warning" | ||
} | ||
}, | ||
"AllowedHosts": "*" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
namespace Application.Interfaces; | ||
|
||
public interface ICallerContext | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using CarsApplication; | ||
using Infrastructure.WebApi.Common; | ||
using Infrastructure.WebApi.Interfaces; | ||
using Infrastructure.WebApi.Interfaces.Operations.Cars; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace CarsApi; | ||
|
||
public class CarsApi : IWebApiService | ||
{ | ||
private readonly ICarsApplication _carsApplication; | ||
|
||
public CarsApi(ICarsApplication carsApplication) | ||
{ | ||
_carsApplication = carsApplication; | ||
} | ||
|
||
[WebApiRoute("/cars/{id}", WebApiOperation.Get)] | ||
public async Task<IResult> Get(GetCarRequest request, CancellationToken cancellationToken) | ||
{ | ||
var car = await _carsApplication.GetCarAsync(new CallerContext(), request.Id, cancellationToken); | ||
return Results.Ok(new GetCarResponse { Car = car }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net7.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\CarsApplication\CarsApplication.csproj" /> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Common\Infrastructure.WebApi.Common.csproj" /> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Interfaces\Infrastructure.WebApi.Interfaces.csproj" /> | ||
</ItemGroup> | ||
|
||
<!-- Runs the source generator (in memory) on build --> | ||
<ItemGroup> | ||
<ProjectReference Include="..\Infrastructure.WebApi.Generators\Infrastructure.WebApi.Generators.csproj" | ||
OutputItemType="Analyzer" | ||
ReferenceOutputAssembly="false" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using Application.Interfaces; | ||
|
||
namespace CarsApplication; | ||
|
||
public class CarsApplication : ICarsApplication | ||
{ | ||
public async Task<string> GetCarAsync(ICallerContext caller, string? id, CancellationToken cancellationToken) | ||
{ | ||
await Task.CompletedTask; | ||
return $"Hello car {id}!"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Application.Interfaces\Application.Interfaces.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
using Application.Interfaces; | ||
|
||
namespace CarsApplication; | ||
|
||
public interface ICarsApplication | ||
{ | ||
Task<string> GetCarAsync(ICallerContext caller, string? id, CancellationToken cancellationToken); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<TargetFramework>net7.0</TargetFramework> | ||
</PropertyGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
namespace Common.Infrastructure.Api.Interfaces; | ||
|
||
public class Class1 | ||
{ | ||
} |
9 changes: 9 additions & 0 deletions
9
src/Common/Infrastructure.Api.Interfaces/Infrastructure.Api.Interfaces.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
</Project> |
Oops, something went wrong.