Skip to content

Commit 554fe51

Browse files
authored
Dev (#26)
* - Removed try block to catch and wrap exception in a Result object in DefaultServiceChannel SendAsync methods. Leave it to caller to handle exceptions. * - simplify empty interface definitions * - add Open API documentation information to docs * - change path of OpenApiDocumentation.md * - add WebResultEndpoint Response Mapping doc * - update individual readme.md asset files for each package * - seperate documentation for service endpoint clients * - bump version
1 parent 81e7dcf commit 554fe51

File tree

15 files changed

+275
-147
lines changed

15 files changed

+275
-147
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>1.1.0</Version>
21+
<Version>1.1.1</Version>
2222
</PropertyGroup>
2323
</Project>

ModEndpoints.sln

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{9FD33E1B-1
5252
docs\EndpointTypes.md = docs\EndpointTypes.md
5353
docs\HandlingFiles.md = docs\HandlingFiles.md
5454
docs\IAsyncEnumerableResponse.md = docs\IAsyncEnumerableResponse.md
55+
docs\OpenApiDocumentation.md = docs\OpenApiDocumentation.md
5556
docs\ParameterBinding.md = docs\ParameterBinding.md
57+
docs\ResultPatternIntegration.md = docs\ResultPatternIntegration.md
5658
docs\RouteGroups.md = docs\RouteGroups.md
59+
docs\ServiceEndpointClients.md = docs\ServiceEndpointClients.md
60+
docs\WebResultEndpointResponseMapping.md = docs\WebResultEndpointResponseMapping.md
5761
EndProjectSection
5862
EndProject
5963
Global

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,14 @@ For more examples, refer to the following:
232232

233233
- [Parameter Binding](./docs/ParameterBinding.md)
234234
- [Route Groups](./docs/RouteGroups.md)
235-
- [IAsyncEnumerable Response](./docs/IAsyncEnumerableResponse.md)
236-
- [Handling Files](./docs/HandlingFiles.md)
237235
- [Disabling Components](./docs/DisablingComponents.md)
238-
- [Result Pattern Integration](./docs/ResultPatternIntegration.md)
236+
- [Open API Documentation](./docs/OpenApiDocumentation.md)
237+
- [Handling Files](./docs/HandlingFiles.md)
239238
- [Endpoint Types](./docs/EndpointTypes.md)
239+
- [Result Pattern Integration](./docs/ResultPatternIntegration.md)
240+
- [IAsyncEnumerable Response](./docs/IAsyncEnumerableResponse.md)
241+
- [WebResultEndpoint Response Mapping](./docs/WebResultEndpointResponseMapping.md)
242+
- [ServiceEndpoint Clients](./docs/ServiceEndpointClients.md)
240243

241244
---
242245

docs/EndpointTypes.md

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -78,56 +78,4 @@ A ServiceEndpoint has following special traits and constraints:
7878
- A ServiceEndpoint's request is specific to that endpoint. Each endpoint must have its unique request type.
7979
- To utilize the advantages of a ServiceEndpoint over other endpoint types, its request and response models have to be shared with clients and therefore has to be in a seperate class library.
8080

81-
These restrictions enable clients to call ServiceEndpoints by utilizing a specialized message channel resolved from dependency injection, with only service base address and endpoint's request/response model information. No other client implementation or knowledge about service is required.
82-
83-
Have a look at [sample ServiceEndpoint implementations](../samples/ShowcaseWebApi/Features/StoresWithServiceEndpoint) along with [sample client implementation](../samples/Client) and [request/response model shared library](../samples/ShowcaseWebApi.FeatureContracts).
84-
85-
## ServiceEndpoint clients
86-
87-
A client application consuming ServiceEndpoints, creates service channel registry entries for those endpoints (remote services) during application startup, basically mapping which request message type will be sent by using which HttpClient configuration (base address, timeout, handlers, resilience definitions, etc.). Service channel utilizes IHttpClientFactory and HttpClient underneath and is configured similarly.
88-
89-
Registration can be done either for all service requests in an assembly...
90-
```csharp
91-
var baseAddress = "https://...";
92-
var clientName = "MyClient";
93-
builder.Services.AddRemoteServicesWithNewClient(
94-
typeof(ListStoresRequest).Assembly,
95-
clientName,
96-
(sp, client) =>
97-
{
98-
client.BaseAddress = new Uri(baseAddress);
99-
client.Timeout = TimeSpan.FromSeconds(5);
100-
},
101-
clientBuilder => clientBuilder.AddTransientHttpErrorPolicy(
102-
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))));
103-
```
104-
105-
or alternatively, remote services can be registered one by one, adding each service request individually...
106-
```csharp
107-
var baseAddress = "https://...";
108-
var clientName = "MyClient";
109-
builder.Services.AddRemoteServiceWithNewClient<ListStoresRequest>(clientName,
110-
(sp, client) =>
111-
{
112-
client.BaseAddress = new Uri(baseAddress);
113-
client.Timeout = TimeSpan.FromSeconds(5);
114-
},
115-
clientBuilder => clientBuilder.AddTransientHttpErrorPolicy(
116-
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))));
117-
builder.Services.AddRemoteServiceToExistingClient<GetStoreByIdRequest>(clientName);
118-
builder.Services.AddRemoteServiceToExistingClient<DeleteStoreRequest>(clientName);
119-
builder.Services.AddRemoteServiceToExistingClient<CreateStoreRequest>(clientName);
120-
builder.Services.AddRemoteServiceToExistingClient<UpdateStoreRequest>(clientName);
121-
```
122-
123-
Then remote services are called with IServiceChannel instance resolved from DI...
124-
```csharp
125-
using IServiceScope serviceScope = hostProvider.CreateScope();
126-
IServiceProvider provider = serviceScope.ServiceProvider;
127-
128-
//resolve service channel from DI
129-
var channel = provider.GetRequiredService<IServiceChannel>();
130-
//send request over channel to remote service
131-
var listResult = await channel.SendAsync(new ListStoresRequest(), ct);
132-
133-
```
81+
These restrictions enable clients to call ServiceEndpoints by utilizing a specialized message channel resolved from dependency injection, see [ServiceEndpoint Clients](ServiceEndpointClients.md) documentation for details.

docs/OpenApiDocumentation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Open API Documentation
2+
3+
API documentation follows the same approach as .NET Core Minimal APIs. You only need to use your preferred documentation generator; no additional packages are required.
4+
5+
[ShowcaseWebApi](./samples/ShowcaseWebApi) sample project uses `Swashbuckle.AspNetCore`.
6+
7+
[WeatherForecastWebApi](./samples/WeatherForecastWebApi) sample project uses `Microsoft.AspNetCore.OpenApi` and `Scalar.AspNetCore`.

docs/ServiceEndpointClients.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ServiceEndpoint Clients
2+
3+
A client application can consume ServiceEndpoints by utilizing a specialized message channel resolved from dependency injection. Only service's base address and endpoint's request/response model information are needed. No other client implementation or service specific knowledge is required.
4+
5+
## ⚙️ Workflow
6+
7+
The workflow for consuming ServiceEndpoints from a client application consists of two main steps: **registration** and **consumption**.
8+
9+
### Registration
10+
11+
A client application registers service endpoints with a specific HttpClient configuration (such as base address, timeout, handlers, resilience policies, etc.) during application startup. This can be performed for all service requests within an assembly or for each request type individually.
12+
13+
To register all service requests in an assembly at once:
14+
15+
```csharp
16+
var baseAddress = "https://...";
17+
var clientName = "MyClient";
18+
builder.Services.AddRemoteServicesWithNewClient(
19+
typeof(ListStoresRequest).Assembly,
20+
clientName,
21+
(sp, client) =>
22+
{
23+
client.BaseAddress = new Uri(baseAddress);
24+
client.Timeout = TimeSpan.FromSeconds(5);
25+
},
26+
clientBuilder => clientBuilder.AddTransientHttpErrorPolicy(
27+
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))));
28+
```
29+
30+
...or to register service requests individually:
31+
32+
```csharp
33+
var baseAddress = "https://...";
34+
var clientName = "MyClient";
35+
builder.Services.AddRemoteServiceWithNewClient<ListStoresRequest>(clientName,
36+
(sp, client) =>
37+
{
38+
client.BaseAddress = new Uri(baseAddress);
39+
client.Timeout = TimeSpan.FromSeconds(5);
40+
},
41+
clientBuilder => clientBuilder.AddTransientHttpErrorPolicy(
42+
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))));
43+
builder.Services.AddRemoteServiceToExistingClient<GetStoreByIdRequest>(clientName);
44+
builder.Services.AddRemoteServiceToExistingClient<DeleteStoreRequest>(clientName);
45+
builder.Services.AddRemoteServiceToExistingClient<CreateStoreRequest>(clientName);
46+
builder.Services.AddRemoteServiceToExistingClient<UpdateStoreRequest>(clientName);
47+
```
48+
>**Notes**:
49+
>1. The client name is used to resolve the correct HttpClient configuration for each request type.
50+
>2. Service channel utilizes IHttpClientFactory and HttpClient underneath and is configured similarly.
51+
52+
### Comsumption
53+
54+
Remote `ServiceEndpoints` can be called by resolving an `IServiceChannel` instance from dependency injection and invoking the `SendAsync` method with the desired request object.
55+
56+
```csharp
57+
using IServiceScope serviceScope = hostProvider.CreateScope();
58+
IServiceProvider provider = serviceScope.ServiceProvider;
59+
60+
//resolve service channel from DI
61+
var channel = provider.GetRequiredService<IServiceChannel>();
62+
//send request over channel to remote service
63+
var listResult = await channel.SendAsync(new ListStoresRequest(), ct);
64+
65+
```
66+
67+
## 📚 Samples
68+
1. [ServiceEndpoint implementations](../samples/ShowcaseWebApi/Features/StoresWithServiceEndpoint)
69+
2. [Client implementation](../samples/ServiceEndpointClient)
70+
3. [Shared library for request/response models](../samples/ShowcaseWebApi.FeatureContracts).
71+
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# WebResultEndpoint Response Mapping
2+
3+
`WebResultEndpoint` transforms the result returned by its `HandleAsync` method —a business result— into appropriate HTTP status code and response format, providing consistent and type-safe API behavior. This process relies on a default result-to-response mapper.
4+
5+
There are several ways to customize how responses are mapped:
6+
7+
One way to customize response mapping is by overriding the ConvertResultToResponseAsync method. This method is invoked after HandleAsync to transform the business result into an HTTP response. By default, it uses the IResultToResponseMapper service for this conversion, but you can override it to implement your own mapping logic.
8+
9+
``` csharp
10+
internal class GetBookById(ServiceDbContext db)
11+
: WebResultEndpoint<GetBookByIdRequest, GetBookByIdResponse>
12+
{
13+
protected override void Configure(
14+
IServiceProvider serviceProvider,
15+
IRouteGroupConfigurator? parentRouteGroup)
16+
{
17+
MapGet("/{Id}")
18+
.Produces<GetBookByIdResponse>();
19+
}
20+
21+
protected override ValueTask<IResult> ConvertResultToResponseAsync(
22+
Result<GetBookByIdResponse> result,
23+
HttpContext context,
24+
CancellationToken ct)
25+
{
26+
// Custom mapping logic here
27+
28+
}
29+
30+
protected override async Task<Result<GetBookByIdResponse>> HandleAsync(
31+
GetBookByIdRequest req,
32+
CancellationToken ct)
33+
{
34+
// implementation
35+
36+
}
37+
}
38+
```
39+
40+
Another option is to implement your own `IResultToResponseMapper` and register it in the DI container as a keyed service using a string key.
41+
You can then apply the `[ResultToResponseMapper("MyMapperKey")]` attribute to any `WebResultEndpoint`-based endpoint to have it use your custom mapper instead of the default one.
42+
43+
``` csharp
44+
// Lifetime of the mapper is defined as singleton only for example purposes, it can be any lifetime
45+
services.TryAddKeyedSingleton<IResultToResponseMapper, MyResultToResponseMapper>("MyMapper");
46+
```
47+
48+
``` csharp
49+
[ResultToResponseMapper("MyMapper")]
50+
internal class GetBookById
51+
: WebResultEndpoint<GetBookByIdRequest, GetBookByIdResponse>
52+
{
53+
protected override void Configure(
54+
IServiceProvider serviceProvider,
55+
IRouteGroupConfigurator? parentRouteGroup)
56+
{
57+
MapGet("/{Id}")
58+
.Produces<GetBookByIdResponse>();
59+
}
60+
61+
protected override async Task<Result<GetBookByIdResponse>> HandleAsync(
62+
GetBookByIdRequest req,
63+
CancellationToken ct)
64+
{
65+
// implementation
66+
67+
}
68+
}
69+
```
70+
71+
It is also possible to replace default mapper for all endpoints by registering your mapper as the default mapper before invoking any variant of `AddModEndpoints` method.
72+
73+
``` csharp
74+
var builder = WebApplication.CreateBuilder(args);
75+
76+
77+
// Register your custom mapper as the default mapper
78+
// Lifetime of the mapper is defined as singleton only for example purposes, it can be any lifetime
79+
services.TryAddKeyedSingleton<IResultToResponseMapper, MyResultToResponseMapper>(
80+
WebResultEndpointDefinitions.DefaultResultToResponseMapperName);
81+
82+
builder.Services.AddModEndpointsFromAssemblyContaining<MyEndpoint>();
83+
//Validation
84+
builder.Services.AddValidatorsFromAssemblyContaining<MyValidator>(includeInternalTypes: true);
85+
86+
var app = builder.Build();
87+
88+
app.MapModEndpoints();
89+
90+
app.Run();
91+
```
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
namespace ModEndpoints.Core;
22

3-
public interface IBusinessResultEndpoint
4-
{
5-
}
3+
public interface IBusinessResultEndpoint;
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
namespace ModEndpoints.Core;
22

3-
public interface IMinimalEndpoint
4-
{
5-
}
3+
public interface IMinimalEndpoint;
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
namespace ModEndpoints.Core;
22

3-
public interface IServiceEndpoint
4-
{
5-
}
3+
public interface IServiceEndpoint;

0 commit comments

Comments
 (0)