Skip to content

Commit 1d23e63

Browse files
committed
Adding Run and RunAsync
1 parent 58b55fd commit 1d23e63

File tree

7 files changed

+1942
-81
lines changed

7 files changed

+1942
-81
lines changed

README.md

Lines changed: 87 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
A dotnet library that allows you to build WebApiEndpoints using a vertical slice architecture approach. Built on dotnet 7 and minimal apis.
99

10-
- [x] Vertical Slice Architecture, giving you the ability to add new features without changing existing ones
11-
- [x] Autodiscovery of WebApiEndpoint, based on Source Generators
10+
- [x] Vertical Slice Architecture, gives you the ability to add new features without changing existing code
1211
- [x] [Easy setup](#easy-setup)
1312
- [x] Full support and built on top of [minimal apis](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0)
1413
- [x] Full support for OpenApi
@@ -24,19 +23,20 @@ A dotnet library that allows you to build WebApiEndpoints using a vertical slice
2423
- [x] [Tested solution](https://coveralls.io/github/futurum-dev/dotnet.futurum.webapiendpoint.micro)
2524
- [x] [Comprehensive samples](#comprehensive-samples)
2625
- [x] [Convention Customisation](#convention-customisation)
26+
- [x] Autodiscovery of WebApiEndpoint, based on Source Generators
2727

2828
## What is a WebApiEndpoint?
2929
- It's a vertical slice / feature of your application
3030
- The vertical slice is a self-contained unit of functionality
3131
- Collection of WebApis that share a route prefix and version
3232

3333
## Easy setup
34-
- [x] Add the [NuGet package](https://www.nuget.org/packages/futurum.webapiendpoint.micro) to your project
34+
- [x] Add the [NuGet package](https://www.nuget.org/packages/futurum.webapiendpoint.micro) ( futurum.webapiendpoint.micro ) to your project
3535
- [x] Update *program.cs* as per [here](#programcs)
3636
- [x] Create a new class that implements *IWebApiEndpoint*
3737
- [x] Add the *WebApiEndpoint* attribute to the class, if you want to specify a specific *route prefix* and *tag*
3838
- [x] Add the *WebApiEndpointVersion* attribute to the class, if you want to specify a specific *ApiVersion*
39-
- [x] Implement the *Register* and add *minimal api* as per usual
39+
- [x] Implement the *Register* and add *minimal api(s)* as per usual
4040

4141
### program.cs
4242
#### AddWebApiEndpoints
@@ -68,6 +68,8 @@ builder.Services.AddWebApiEndpoints(new WebApiEndpointConfiguration(WebApiEndpoi
6868
```
6969

7070
#### AddWebApiEndpointsFor... (per project containing WebApiEndpoints)
71+
This will be automatically created by the source generator.
72+
7173
e.g.
7274
```csharp
7375
builder.Services.AddWebApiEndpointsForFuturumWebApiEndpointMicroSample();
@@ -117,39 +119,13 @@ app.Run();
117119
```
118120

119121
### IWebApiEndpoint
120-
### Configure
121-
You can configure the WebApiEndpoint in the *Configure* method
122-
123-
```csharp
124-
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
125-
{
126-
}
127-
```
128-
129-
This allows you to set properties on the RouteGroupBuilder.
130-
131-
You can also configure it differently per ApiVersion.
132-
133-
**NOTE:** this is optional
134-
135-
**NOTE:** this ia a good place to add *EndpointFilter*
136-
```csharp
137-
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
138-
{
139-
groupBuilder.AddEndpointFilter<CustomEndpointFilter>();
140-
}
141-
```
142-
143-
**NOTE:** this ia a good place to add *Security*
144-
```csharp
145-
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
146-
{
147-
groupBuilder.RequireAuthorization(Authorization.Permission.Admin);
148-
}
149-
```
150-
151122
### Register
152-
You can register the WebApiEndpoint in the *Register* method
123+
You can *map* your minimal apis for this WebApiEndpoint in the *Register* method.
124+
125+
The *builder* parameter is already:
126+
- configured with the API versioning
127+
- configured with the route prefix
128+
- gone through the *Configure* method in the same class (if there is one)
153129

154130
```csharp
155131
public void Register(IEndpointRouteBuilder builder)
@@ -211,6 +187,37 @@ public class BytesWebApiEndpoint : IWebApiEndpoint
211187
}
212188
```
213189

190+
### Configure
191+
You can configure the WebApiEndpoint in the *Configure* method
192+
193+
```csharp
194+
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
195+
{
196+
}
197+
```
198+
199+
This allows you to set properties on the RouteGroupBuilder.
200+
201+
You can also configure it differently per ApiVersion.
202+
203+
**NOTE:** this is optional
204+
205+
**NOTE:** this ia a good place to add *EndpointFilter*
206+
```csharp
207+
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
208+
{
209+
groupBuilder.AddEndpointFilter<CustomEndpointFilter>();
210+
}
211+
```
212+
213+
**NOTE:** this ia a good place to add *Security*
214+
```csharp
215+
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
216+
{
217+
groupBuilder.RequireAuthorization(Authorization.Permission.Admin);
218+
}
219+
```
220+
214221
## Validation
215222
### ValidationService
216223
Executes FluentValidation and DataAnnotations
@@ -316,27 +323,27 @@ Use the *FormFilesWithPayload* type to upload multiple files and a JSON payload
316323
}
317324
```
318325

319-
## Full compatibility with [Futurum.Core](https://www.nuget.org/packages/Futurum.Core)
320-
Comprehensive set of extension methods to transform a [Result](https://docs.futurum.dev/dotnet.futurum.core/result/overview.html) and [Result&lt;T&gt;](https://docs.futurum.dev/dotnet.futurum.core/result/overview.html) to an *TypedResult*.
326+
## Results&lt;...&gt; -> Results&lt;..., BadRequest&lt;ProblemDetails&gt;&gt;
327+
Comprehensive set of extension methods - *WebApiEndpointRunner.Run* and *WebApiEndpointRunner.RunAsync* - to run a method and if it throws an *exception* it will catch and transform it into a *BadRequest&lt;ProblemDetails&gt;*.
321328

322-
#### Result&lt;IResult&gt; -> Results&lt;IResult, BadRequest&lt;ProblemDetails&gt;&gt;
323-
- If the Result&lt;IResult&gt; is a *Success&lt;IResult&gt;* then the *IResult* will be returned.
324-
- If the Result&lt;T&gt; is a *Failure&lt;T&gt;* then the *BadRequest&lt;ProblemDetails&gt;* will be returned, with the appropriate details set on the ProblemDetails. The *error message* will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
329+
The *Run* and *RunAsync* methods will:
330+
- If the method passed in **does not** throw an exception, then the existing return remains the same.
331+
- If the method passed in **does** throw an exception, then a *BadRequest&lt;ProblemDetails&gt;* will be returned, with the appropriate details set on the ProblemDetails. The *error message* will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
325332

326-
This works for:
333+
The returned type from *Run* and *RunAsync* is always augmented to additionally include *BadRequest&lt;ProblemDetails&gt;*
327334

328335
```csharp
329-
Result<IResult>
330-
331-
Result<Results<IResult, IResult>>
336+
T -> Results<T, BadRequest<ProblemDetails>>
332337

333-
Result<Results<IResult, IResult, IResult>>
338+
Results<TIResult1, TIResult2> -> Results<TIResult1, TIResult2, BadRequest<ProblemDetails>>
334339

335-
Result<Results<IResult, IResult, IResult, IResult>>
340+
Results<TIResult1, TIResult2, TIResult3> -> Results<TIResult1, TIResult2, TIResult3, BadRequest<ProblemDetails>>
336341

337-
Result<Results<IResult, IResult, IResult, IResult, IResult>>
342+
Results<TIResult1, TIResult2, TIResult3, TIResult4> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, BadRequest<ProblemDetails>>
338343

344+
Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5, BadRequest<ProblemDetails>>
339345
```
346+
340347
*Results* has a maximum of 6 types. So 5 are allowed leaving one space left for the *BadRequest&lt;ProblemDetails&gt;*.
341348

342349
##### Example use
@@ -348,7 +355,7 @@ In this example the *Execute* method will return:
348355
Results<NotFound, FileStreamHttpResult>
349356
```
350357

351-
The *ToWebApi* extension method will change this to add *BadRequest&lt;ProblemDetails&gt;*.
358+
The *Run* / *RunAsync* extension method will change this to add *BadRequest&lt;ProblemDetails&gt;*.
352359

353360
```csharp
354361
Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>>
@@ -358,8 +365,7 @@ Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>>
358365
```csharp
359366
private static Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>> DownloadHandler(HttpContext context)
360367
{
361-
return Result.Try(Execute, () => "Failed to read file")
362-
.ToWebApi(context);
368+
return Run(Execute, context, "Failed to read file");
363369

364370
Results<NotFound, FileStreamHttpResult> Execute()
365371
{
@@ -376,6 +382,35 @@ private static Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails
376382
}
377383
```
378384

385+
**Note:** It is recommended to add the following to your *GlobalUsings.cs* file.
386+
```csharp
387+
global using static Futurum.WebApiEndpoint.Micro.WebApiEndpointRunner;
388+
```
389+
390+
This means you can use the helper functions without having to specify the namespace. As in the examples.
391+
392+
## Full compatibility with [Futurum.Core](https://www.nuget.org/packages/Futurum.Core)
393+
Comprehensive set of extension methods to transform a [Result](https://docs.futurum.dev/dotnet.futurum.core/result/overview.html) and [Result&lt;T&gt;](https://docs.futurum.dev/dotnet.futurum.core/result/overview.html) to an *TypedResult*.
394+
395+
- If the method passed in is a *success*, then the *IResult* will be returned.
396+
- If the method passed in is a *failure*, then a *BadRequest&lt;ProblemDetails&gt;* will be returned, with the appropriate details set on the ProblemDetails. The *error message* will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
397+
398+
The returned type from *ToWebApi* is always augmented to additionally include *BadRequest&lt;ProblemDetails&gt;*
399+
400+
```csharp
401+
Result<T> -> Results<T, BadRequest<ProblemDetails>>
402+
403+
Result<Results<TIResult1, TIResult2>> -> Results<TIResult1, TIResult2, BadRequest<ProblemDetails>>
404+
405+
Result<Results<TIResult1, TIResult2, TIResult3>> -> Results<TIResult1, TIResult2, TIResult3, BadRequest<ProblemDetails>>
406+
407+
Result<Results<TIResult1, TIResult2, TIResult3, TIResult4>> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, BadRequest<ProblemDetails>>
408+
409+
Result<Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5>> -> Results<TIResult1, TIResult2, TIResult3, TIResult5, BadRequest<ProblemDetails>>
410+
```
411+
412+
*Results* has a maximum of 6 types. So 5 are allowed leaving one space left for the *BadRequest&lt;ProblemDetails&gt;*.
413+
379414
#### How to handle *successful* and *failure* cases in a typed way with *TypedResult*
380415
You can optionally specify which TypedResult success cases you want to handle. This is useful if you want to handle a specific successes case differently.
381416

@@ -413,6 +448,7 @@ There can only be 1 *success* helper function, but there can be multiple *failur
413448
```csharp
414449
global using static Futurum.WebApiEndpoint.Micro.WebApiResultsExtensions;
415450
```
451+
416452
This means you can use the helper functions without having to specify the namespace. As in the examples.
417453

418454
#### Success

sample/Futurum.WebApiEndpoint.Micro.Sample/Features/BytesWebApiEndpoint.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ public void Register(IEndpointRouteBuilder builder)
1212

1313
private static Results<NotFound, FileContentHttpResult, BadRequest<ProblemDetails>> DownloadHandler(HttpContext context)
1414
{
15-
return Result.Try(Execute, () => "Failed to read file")
16-
.ToWebApi(context);
15+
return Run(Execute, context, "Failed to read file");
1716

1817
Results<NotFound, FileContentHttpResult> Execute()
1918
{

sample/Futurum.WebApiEndpoint.Micro.Sample/Features/FileWebApiEndpoint.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ public void Register(IEndpointRouteBuilder builder)
2323

2424
private static Task<Results<Ok<FileDetailsDto>, BadRequest<ProblemDetails>>> UploadHandler(HttpContext context, IFormFile file)
2525
{
26-
return Result.TryAsync(Execute, () => "Failed to read file")
27-
.ToWebApiAsync(context, ToOk);
26+
return RunAsync(Execute, context, ToOk, "Failed to read file");
2827

2928
async Task<FileDetailsDto> Execute()
3029
{
@@ -38,8 +37,7 @@ async Task<FileDetailsDto> Execute()
3837

3938
private static Task<Results<Ok<IEnumerable<FileDetailsDto>>, BadRequest<ProblemDetails>>> UploadsHandler(HttpContext context, IFormFileCollection files)
4039
{
41-
return Result.TryAsync(Execute, () => "Failed to read file")
42-
.ToWebApiAsync(context, ToOk);
40+
return RunAsync(Execute, context, ToOk, "Failed to read file");
4341

4442
async Task<IEnumerable<FileDetailsDto>> Execute()
4543
{
@@ -60,8 +58,7 @@ async Task<IEnumerable<FileDetailsDto>> Execute()
6058

6159
private static Task<Results<Ok<FileDetailsWithPayloadDto>, BadRequest<ProblemDetails>>> UploadWithPayloadHandler(HttpContext context, FormFileWithPayload<PayloadDto> fileWithPayload)
6260
{
63-
return Result.TryAsync(Execute, () => "Failed to read file")
64-
.ToWebApiAsync(context, ToOk);
61+
return RunAsync(Execute, context, ToOk, "Failed to read file");
6562

6663
async Task<FileDetailsWithPayloadDto> Execute()
6764
{
@@ -76,8 +73,7 @@ async Task<FileDetailsWithPayloadDto> Execute()
7673
private static Task<Results<Ok<IEnumerable<FileDetailsWithPayloadDto>>, BadRequest<ProblemDetails>>> UploadsWithPayloadHandler(
7774
HttpContext context, FormFilesWithPayload<PayloadDto> filesWithPayload)
7875
{
79-
return Result.TryAsync(Execute, () => "Failed to read file")
80-
.ToWebApiAsync(context, ToOk);
76+
return RunAsync(Execute, context, ToOk, "Failed to read file");
8177

8278
async Task<IEnumerable<FileDetailsWithPayloadDto>> Execute()
8379
{
@@ -98,8 +94,7 @@ async Task<IEnumerable<FileDetailsWithPayloadDto>> Execute()
9894

9995
private static Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>> DownloadHandler(HttpContext context)
10096
{
101-
return Result.Try(Execute, () => "Failed to read file")
102-
.ToWebApi(context);
97+
return Run(Execute, context, "Failed to read file");
10398

10499
Results<NotFound, FileStreamHttpResult> Execute()
105100
{

sample/Futurum.WebApiEndpoint.Micro.Sample/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
global using Microsoft.AspNetCore.Http.HttpResults;
77
global using Microsoft.AspNetCore.Mvc;
88

9+
global using static Futurum.WebApiEndpoint.Micro.WebApiEndpointRunner;
910
global using static Futurum.WebApiEndpoint.Micro.WebApiResultsExtensions;

sample/Futurum.WebApiEndpoint.Micro.Sample/Todo/TodoApiWebApiEndpoint.cs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,35 +43,31 @@ public void Register(IEndpointRouteBuilder builder)
4343

4444
private static Results<Ok<IAsyncEnumerable<Todo>>, BadRequest<ProblemDetails>> GetAllHandler(HttpContext context, SqliteConnection db)
4545
{
46-
return Result.Try(Execute, "Failed to get todos")
47-
.ToWebApi(context, ToOk);
46+
return Run(Execute, context, ToOk, "Failed to get todos");
4847

4948
IAsyncEnumerable<Todo> Execute() =>
5049
db.QueryAsync<Todo>("SELECT * FROM Todos");
5150
}
5251

5352
private static Results<Ok<IAsyncEnumerable<Todo>>, BadRequest<ProblemDetails>> GetCompleteHandler(HttpContext context, SqliteConnection db)
5453
{
55-
return Result.Try(Execute, "Failed to get complete todos")
56-
.ToWebApi(context, ToOk);
54+
return Run(Execute, context, ToOk, "Failed to get complete todos");
5755

5856
IAsyncEnumerable<Todo> Execute() =>
5957
db.QueryAsync<Todo>("SELECT * FROM Todos WHERE IsComplete = true");
6058
}
6159

6260
private static Results<Ok<IAsyncEnumerable<Todo>>, BadRequest<ProblemDetails>> GetIncompleteHandler(HttpContext context, SqliteConnection db)
6361
{
64-
return Result.Try(Execute, "Failed to get incomplete todos")
65-
.ToWebApi(context, ToOk);
62+
return Run(Execute, context, ToOk, "Failed to get incomplete todos");
6663

6764
IAsyncEnumerable<Todo> Execute() =>
6865
db.QueryAsync<Todo>("SELECT * FROM Todos WHERE IsComplete = false");
6966
}
7067

7168
private static Task<Results<Ok<Todo>, NotFound, BadRequest<ProblemDetails>>> GetByIdHandler(HttpContext context, SqliteConnection db, int id)
7269
{
73-
return Result.TryAsync(Execute, $"Failed to find todo with id '{id}'")
74-
.ToWebApiAsync(context);
70+
return RunAsync(Execute, context, () => $"Failed to find todo with id '{id}'");
7571

7672
async Task<Results<Ok<Todo>, NotFound>> Execute() =>
7773
await db.QuerySingleAsync<Todo>("SELECT * FROM Todos WHERE Id = @id", id.AsDbParameter()) is Todo todo
@@ -81,8 +77,7 @@ await db.QuerySingleAsync<Todo>("SELECT * FROM Todos WHERE Id = @id", id.AsDbPar
8177

8278
private static Task<Results<Ok<Todo>, NoContent, BadRequest<ProblemDetails>>> FindHandler(HttpContext context, SqliteConnection db, string title, bool? isComplete)
8379
{
84-
return Result.TryAsync(Execute, $"Failed to find todo with title '{title}'")
85-
.ToWebApiAsync(context);
80+
return RunAsync(Execute, context, () => $"Failed to find todo with title '{title}'");
8681

8782
async Task<Results<Ok<Todo>, NoContent>> Execute() =>
8883
await db.QuerySingleAsync<Todo>("SELECT * FROM Todos WHERE Title = @title COLLATE NOCASE AND (@isComplete is NULL OR IsComplete = @isComplete)",
@@ -126,8 +121,7 @@ await db.ExecuteAsync("UPDATE Todos SET Title = @Title, IsComplete = @IsComplete
126121

127122
private static Task<Results<NoContent, NotFound, BadRequest<ProblemDetails>>> MarkCompleteHandler(HttpContext context, SqliteConnection db, int id)
128123
{
129-
return Result.TryAsync(Execute, $"Failed to mark todo with id '{id}' as incomplete")
130-
.ToWebApiAsync(context);
124+
return RunAsync(Execute, context, () => $"Failed to mark todo with id '{id}' as incomplete");
131125

132126
async Task<Results<NoContent, NotFound>> Execute() =>
133127
await db.ExecuteAsync("UPDATE Todos SET IsComplete = true WHERE Id = @id", id.AsDbParameter()) == 1
@@ -137,8 +131,7 @@ await db.ExecuteAsync("UPDATE Todos SET IsComplete = true WHERE Id = @id", id.As
137131

138132
private static Task<Results<NoContent, NotFound, BadRequest<ProblemDetails>>> MarkIncompleteHandler(HttpContext context, SqliteConnection db, int id)
139133
{
140-
return Result.TryAsync(Execute, $"Failed to mark todo with id '{id}' as incomplete")
141-
.ToWebApiAsync(context);
134+
return RunAsync(Execute, context, () => $"Failed to mark todo with id '{id}' as incomplete");
142135

143136
async Task<Results<NoContent, NotFound>> Execute() =>
144137
await db.ExecuteAsync("UPDATE Todos SET IsComplete = false WHERE Id = @id", id.AsDbParameter()) == 1
@@ -148,8 +141,7 @@ await db.ExecuteAsync("UPDATE Todos SET IsComplete = false WHERE Id = @id", id.A
148141

149142
private static Task<Results<NoContent, NotFound, BadRequest<ProblemDetails>>> DeleteHandler(HttpContext context, SqliteConnection db, int id)
150143
{
151-
return Result.TryAsync(Execute, $"Failed to delete todo with id '{id}'")
152-
.ToWebApiAsync(context);
144+
return RunAsync(Execute, context, () => $"Failed to delete todo with id '{id}'");
153145

154146
async Task<Results<NoContent, NotFound>> Execute() =>
155147
await db.ExecuteAsync("DELETE FROM Todos WHERE Id = @id", id.AsDbParameter()) == 1
@@ -159,8 +151,7 @@ await db.ExecuteAsync("DELETE FROM Todos WHERE Id = @id", id.AsDbParameter()) ==
159151

160152
private static Task<Results<Ok<int>, BadRequest<ProblemDetails>>> DeleteAllHandler(HttpContext context, SqliteConnection db)
161153
{
162-
return Result.TryAsync(Execute, "Failed to delete all todos")
163-
.ToWebApiAsync(context, ToOk);
154+
return RunAsync(Execute, context, ToOk, "Failed to delete all todos");
164155

165156
Task<int> Execute() =>
166157
db.ExecuteAsync("DELETE FROM Todos");

0 commit comments

Comments
 (0)