Skip to content

Commit 5413577

Browse files
authored
Dev (#12)
* - MinimalEndpoints: add support for invalid response message for MinimalEndpoints that use TypedResults - add Customers feature that use MinimalEndpoints to ShowcaseWebApi project * - code cleanup * - seed customers table on application startup - update reaadme * - bump version * - use already resolved response type instead of calling typeof() again
1 parent 652ae94 commit 5413577

File tree

13 files changed

+412
-8
lines changed

13 files changed

+412
-8
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1919
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
2020

21-
<Version>0.6.2</Version>
21+
<Version>0.6.3</Version>
2222
</PropertyGroup>
2323
</Project>

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
To make consuming a [ServiceEndpoint](#serviceendpoint) easier, which is a very specialized endpoint more suitable for internal services, a specific [client implementation](#serviceendpoint-clients) along with extensions required for client registration is implemented in ModEndpoints.RemoteServices package, and interfaces required for ServiceEndpoint request models are in ModEndpoints.RemoteServices.Core package.
1212

13-
[ShowcaseWebApi](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi) project demonstrates various kinds of endpoint implementations and configurations. [Client](https://github.com/modabas/ModEndpoints/tree/main/samples/Client) project is a sample ServiceEndpoint consumer.
13+
Each of them are demonstrated in [sample projects](#samples).
1414

1515
All endpoint abstractions are a structured approach to defining endpoints in ASP.NET Core applications. They extend the Minimal Api pattern with reusable, testable, and consistent components for request handling, validation, and response mapping.
1616

@@ -308,6 +308,16 @@ internal class CreateBook(ServiceDbContext db, ILocationStore location)
308308
}
309309
```
310310

311+
## Samples
312+
313+
[ShowcaseWebApi](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi) project demonstrates various kinds of endpoint implementations and configurations:
314+
- MinimalEnpoints samples are in [Customers](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi/Features/Customers) subfolder,
315+
- WebResultEndpoints samples are in [Books](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi/Features/Books) subfolder,
316+
- BusinessResultEndpoints samples are in [Stores](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi/Features/Stores) subfolder,
317+
- ServiceEndpoints samples are in [StoresWithServiceEndpoint](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi/Features/StoresWithServiceEndpoint) subfolder.
318+
319+
[ServiceEndpointClient](https://github.com/modabas/ModEndpoints/tree/main/samples/ServiceEndpointClient) project demonstrates how to consume ServiceEndpoints.
320+
311321
## Performance
312322

313323
WebResultEndpoints have a slight overhead (3-4%) over regular Minimal Apis on request/sec metric under load tests with 100 virtual users.
@@ -326,9 +336,9 @@ MinimalEndpoint within ModEndpoints.Core package, is closest to barebones Minima
326336

327337
- string
328338
- T (Any other type)
329-
- Minimal Api IResult based
339+
- Minimal Api IResult based (Including TypedResults with Results<TResult1, TResultN> return value)
330340

331-
Other features described previously are common for all of them.
341+
See (How to create responses in Minimal API apps)[https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses?view=aspnetcore-8.0] for detailed information. Other features described previously are common for all of them.
332342

333343
Each type of endpoint has various implementations that accept a request model or not, that has a response model or not.
334344

samples/ShowcaseWebApi/Data/ServiceDbContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.EntityFrameworkCore;
22
using ShowcaseWebApi.Features.Books.Data;
3+
using ShowcaseWebApi.Features.Customers.Data;
34
using ShowcaseWebApi.Features.Stores.Data;
45

56
namespace ShowcaseWebApi.Data;
@@ -23,5 +24,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
2324
#region DbSets
2425
public DbSet<BookEntity> Books => Set<BookEntity>();
2526
public DbSet<StoreEntity> Stores => Set<StoreEntity>();
27+
public DbSet<CustomerEntity> Customers => Set<CustomerEntity>();
2628
#endregion
2729
}

samples/ShowcaseWebApi/Extensions/WebApplicationExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ShowcaseWebApi.Data;
22
using ShowcaseWebApi.Features.Books.Data;
3+
using ShowcaseWebApi.Features.Customers.Data;
34
using ShowcaseWebApi.Features.Stores.Data;
45

56
namespace ShowcaseWebApi.Extensions;
@@ -41,6 +42,23 @@ public static void SeedData(this WebApplication app)
4142
{
4243
Name = "Middling Evil Store"
4344
});
45+
db.Customers.Add(new CustomerEntity()
46+
{
47+
FirstName = "Willie",
48+
MiddleName = "Jonathan",
49+
LastName = "Normand"
50+
});
51+
db.Customers.Add(new CustomerEntity()
52+
{
53+
FirstName = "Leslie",
54+
MiddleName = "Lois",
55+
LastName = "Coffman"
56+
});
57+
db.Customers.Add(new CustomerEntity()
58+
{
59+
FirstName = "Oliver",
60+
LastName = "Rogers"
61+
});
4462
db.SaveChanges();
4563
}
4664
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using ModEndpoints.Core;
2+
3+
namespace ShowcaseWebApi.Features.Customers.Configuration;
4+
5+
[MapToGroup<FeaturesRouteGroup>()]
6+
internal class CustomersV1RouteGroup : RouteGroupConfigurator
7+
{
8+
protected override void Configure(
9+
IServiceProvider serviceProvider,
10+
IRouteGroupConfigurator? parentRouteGroup)
11+
{
12+
MapGroup("/customers")
13+
.MapToApiVersion(1)
14+
.WithTags("/CustomersV1");
15+
}
16+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using FluentValidation;
2+
using Microsoft.AspNetCore.Http.HttpResults;
3+
using Microsoft.AspNetCore.Mvc;
4+
using ModEndpoints.Core;
5+
using ShowcaseWebApi.Data;
6+
using ShowcaseWebApi.Features.Customers.Configuration;
7+
using ShowcaseWebApi.Features.Customers.Data;
8+
9+
namespace ShowcaseWebApi.Features.Customers;
10+
public record CreateCustomerRequest([FromBody] CreateCustomerRequestBody Body);
11+
12+
public record CreateCustomerRequestBody(string FirstName, string? MiddleName, string LastName);
13+
14+
public record CreateCustomerResponse(Guid Id);
15+
16+
internal class CreateCustomerRequestValidator : AbstractValidator<CreateCustomerRequest>
17+
{
18+
public CreateCustomerRequestValidator()
19+
{
20+
RuleFor(x => x.Body.FirstName).NotEmpty();
21+
RuleFor(x => x.Body.LastName).NotEmpty();
22+
}
23+
}
24+
25+
[MapToGroup<CustomersV1RouteGroup>()]
26+
internal class CreateCustomer(ServiceDbContext db)
27+
: MinimalEndpoint<CreateCustomerRequest, Results<CreatedAtRoute<CreateCustomerResponse>, ValidationProblem>>
28+
{
29+
protected override void Configure(
30+
IServiceProvider serviceProvider,
31+
IRouteGroupConfigurator? parentRouteGroup)
32+
{
33+
MapPost("/");
34+
}
35+
36+
protected override async Task<Results<CreatedAtRoute<CreateCustomerResponse>, ValidationProblem>> HandleAsync(
37+
CreateCustomerRequest req,
38+
CancellationToken ct)
39+
{
40+
var customer = new CustomerEntity()
41+
{
42+
FirstName = req.Body.FirstName,
43+
MiddleName = req.Body.MiddleName,
44+
LastName = req.Body.LastName
45+
};
46+
47+
db.Customers.Add(customer);
48+
await db.SaveChangesAsync(ct);
49+
50+
return TypedResults.CreatedAtRoute(
51+
new CreateCustomerResponse(customer.Id),
52+
typeof(GetCustomerById).FullName,
53+
new { id = customer.Id });
54+
}
55+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using ShowcaseWebApi.Data;
2+
3+
namespace ShowcaseWebApi.Features.Customers.Data;
4+
5+
internal class CustomerEntity : BaseEntity
6+
{
7+
public string FirstName { get; set; } = string.Empty;
8+
public string? MiddleName { get; set; }
9+
public string LastName { get; set; } = string.Empty;
10+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using FluentValidation;
2+
using Microsoft.EntityFrameworkCore;
3+
using ModEndpoints.Core;
4+
using ShowcaseWebApi.Data;
5+
using ShowcaseWebApi.Features.Customers.Configuration;
6+
7+
namespace ShowcaseWebApi.Features.Customers;
8+
public record DeleteCustomerRequest(Guid Id);
9+
10+
internal class DeleteCustomerRequestValidator : AbstractValidator<DeleteCustomerRequest>
11+
{
12+
public DeleteCustomerRequestValidator()
13+
{
14+
RuleFor(x => x.Id).NotEmpty();
15+
}
16+
}
17+
18+
[MapToGroup<CustomersV1RouteGroup>()]
19+
internal class DeleteCustomer(ServiceDbContext db)
20+
: MinimalEndpoint<DeleteCustomerRequest, IResult>
21+
{
22+
protected override void Configure(
23+
IServiceProvider serviceProvider,
24+
IRouteGroupConfigurator? parentRouteGroup)
25+
{
26+
MapDelete("/{Id}")
27+
.Produces(StatusCodes.Status204NoContent);
28+
}
29+
30+
protected override async Task<IResult> HandleAsync(
31+
DeleteCustomerRequest req,
32+
CancellationToken ct)
33+
{
34+
var entity = await db.Customers.FirstOrDefaultAsync(b => b.Id == req.Id, ct);
35+
36+
if (entity is null)
37+
{
38+
return Results.NotFound();
39+
}
40+
41+
db.Customers.Remove(entity);
42+
var deleted = await db.SaveChangesAsync(ct);
43+
return deleted > 0 ? Results.NoContent() : Results.NotFound();
44+
}
45+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using FluentValidation;
2+
using Microsoft.AspNetCore.Http.HttpResults;
3+
using Microsoft.EntityFrameworkCore;
4+
using ModEndpoints.Core;
5+
using ShowcaseWebApi.Data;
6+
using ShowcaseWebApi.Features.Customers.Configuration;
7+
8+
namespace ShowcaseWebApi.Features.Customers;
9+
10+
public record GetCustomerByIdRequest(Guid Id);
11+
12+
public record GetCustomerByIdResponse(Guid Id, string FirstName, string? MiddleName, string LastName);
13+
14+
internal class GetCustomerByIdRequestValidator : AbstractValidator<GetCustomerByIdRequest>
15+
{
16+
public GetCustomerByIdRequestValidator()
17+
{
18+
RuleFor(x => x.Id).NotEmpty();
19+
}
20+
}
21+
22+
[MapToGroup<CustomersV1RouteGroup>()]
23+
internal class GetCustomerById(ServiceDbContext db)
24+
: MinimalEndpoint<GetCustomerByIdRequest, Results<Ok<GetCustomerByIdResponse>, NotFound, ValidationProblem>>
25+
{
26+
protected override void Configure(
27+
IServiceProvider serviceProvider,
28+
IRouteGroupConfigurator? parentRouteGroup)
29+
{
30+
MapGet("/{Id}");
31+
}
32+
33+
protected override async Task<Results<Ok<GetCustomerByIdResponse>, NotFound, ValidationProblem>> HandleAsync(
34+
GetCustomerByIdRequest req,
35+
CancellationToken ct)
36+
{
37+
var entity = await db.Customers.FirstOrDefaultAsync(b => b.Id == req.Id, ct);
38+
39+
return entity is null ?
40+
TypedResults.NotFound() :
41+
TypedResults.Ok(new GetCustomerByIdResponse(
42+
Id: entity.Id,
43+
FirstName: entity.FirstName,
44+
MiddleName: entity.MiddleName,
45+
LastName: entity.LastName));
46+
}
47+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using ModEndpoints.Core;
3+
using ShowcaseWebApi.Data;
4+
using ShowcaseWebApi.Features.Customers.Configuration;
5+
6+
namespace ShowcaseWebApi.Features.Customers;
7+
8+
public record ListCustomersResponse(List<ListCustomersResponseItem> Customers);
9+
public record ListCustomersResponseItem(
10+
Guid Id,
11+
string FirstName,
12+
string? MiddleName,
13+
string LastName);
14+
15+
[MapToGroup<CustomersV1RouteGroup>()]
16+
internal class ListCustomers(ServiceDbContext db)
17+
: MinimalEndpoint<ListCustomersResponse>
18+
{
19+
protected override void Configure(
20+
IServiceProvider serviceProvider,
21+
IRouteGroupConfigurator? parentRouteGroup)
22+
{
23+
MapGet("/");
24+
}
25+
26+
protected override async Task<ListCustomersResponse> HandleAsync(
27+
CancellationToken ct)
28+
{
29+
var customers = await db.Customers
30+
.Select(c => new ListCustomersResponseItem(
31+
c.Id,
32+
c.FirstName,
33+
c.MiddleName,
34+
c.LastName))
35+
.ToListAsync(ct);
36+
37+
return new ListCustomersResponse(Customers: customers);
38+
}
39+
}

0 commit comments

Comments
 (0)