Skip to content

Commit da01dc7

Browse files
authored
Merge pull request #1 from modabas/businessResultEndpoint
Business result endpoint
2 parents 9944bda + dfa13b1 commit da01dc7

File tree

60 files changed

+1363
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1363
-80
lines changed

.github/workflows/release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
env:
88
COREPROJECTNAME: "ModEndpoints.Core"
99
ENDPOINTSPROJECTNAME: "ModEndpoints"
10+
REMOTESERVICESCOREPROJECTNAME: "ModEndpoints.RemoteServices.Core"
11+
REMOTESERVICESPROJECTNAME: "ModEndpoints.RemoteServices"
1012

1113
jobs:
1214
build:
@@ -25,12 +27,24 @@ jobs:
2527
9.0.x
2628
- name: Restore dependencies
2729
run: dotnet restore
30+
- name: Build
31+
run: dotnet build ${{ env.REMOTESERVICESCOREPROJECTNAME }} --configuration Release --no-restore
32+
working-directory: src
33+
- name: Build
34+
run: dotnet build ${{ env.REMOTESERVICESPROJECTNAME }} --configuration Release --no-restore
35+
working-directory: src
2836
- name: Build
2937
run: dotnet build ${{ env.COREPROJECTNAME }} --configuration Release --no-restore
3038
working-directory: src
3139
- name: Build
3240
run: dotnet build ${{ env.ENDPOINTSPROJECTNAME }} --configuration Release --no-restore
3341
working-directory: src
42+
- name: Package nuget remote services core
43+
run: dotnet pack ${{ env.REMOTESERVICESCOREPROJECTNAME }} --configuration Release --no-build -o:package
44+
working-directory: src
45+
- name: Package nuget remote services
46+
run: dotnet pack ${{ env.REMOTESERVICESPROJECTNAME }} --configuration Release --no-build -o:package
47+
working-directory: src
3448
- name: Package nuget endpoints core
3549
run: dotnet pack ${{ env.COREPROJECTNAME }} --configuration Release --no-build -o:package
3650
working-directory: src

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
<PackageReadmeFile>README.md</PackageReadmeFile>
1616
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1717

18-
<Version>0.2.0</Version>
18+
<Version>0.3.0</Version>
1919
</PropertyGroup>
2020
</Project>

ModEndpoints.sln

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModEndpoints.Core", "src\Mo
3131
EndProject
3232
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModEndpoints", "src\ModEndpoints\ModEndpoints.csproj", "{0F89CC73-32D4-4347-B284-41804A8A54A9}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModEndpoints.RemoteServices.Core", "src\ModEndpoints.RemoteServices.Core\ModEndpoints.RemoteServices.Core.csproj", "{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67}"
35+
EndProject
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModEndpoints.RemoteServices", "src\ModEndpoints.RemoteServices\ModEndpoints.RemoteServices.csproj", "{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB}"
37+
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShowcaseWebApi.FeatureContracts", "samples\ShowcaseWebApi.FeatureContracts\ShowcaseWebApi.FeatureContracts.csproj", "{DE3AA974-14C3-402F-93F4-A4A5D3DC0131}"
39+
EndProject
3440
Global
3541
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3642
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +63,18 @@ Global
5763
{0F89CC73-32D4-4347-B284-41804A8A54A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
5864
{0F89CC73-32D4-4347-B284-41804A8A54A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
5965
{0F89CC73-32D4-4347-B284-41804A8A54A9}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67}.Release|Any CPU.Build.0 = Release|Any CPU
70+
{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71+
{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
72+
{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
73+
{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB}.Release|Any CPU.Build.0 = Release|Any CPU
74+
{DE3AA974-14C3-402F-93F4-A4A5D3DC0131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75+
{DE3AA974-14C3-402F-93F4-A4A5D3DC0131}.Debug|Any CPU.Build.0 = Debug|Any CPU
76+
{DE3AA974-14C3-402F-93F4-A4A5D3DC0131}.Release|Any CPU.ActiveCfg = Release|Any CPU
77+
{DE3AA974-14C3-402F-93F4-A4A5D3DC0131}.Release|Any CPU.Build.0 = Release|Any CPU
6078
EndGlobalSection
6179
GlobalSection(SolutionProperties) = preSolution
6280
HideSolutionNode = FALSE
@@ -68,5 +86,8 @@ Global
6886
{78546E80-4971-4C23-B8CE-D64FCB34F1A1} = {74B81852-D3A9-49BA-A62F-A653FBB36665}
6987
{3AE2AB1C-C9CE-4F1D-8D73-2BD13864F0D5} = {04A4BF42-A7C5-4D83-A137-90D6C3E68A00}
7088
{0F89CC73-32D4-4347-B284-41804A8A54A9} = {04A4BF42-A7C5-4D83-A137-90D6C3E68A00}
89+
{80C3DA6D-EDBE-47A5-BC6B-93BC334BEF67} = {04A4BF42-A7C5-4D83-A137-90D6C3E68A00}
90+
{3CDE6A69-09BA-4714-8DCD-D934BA27EBEB} = {04A4BF42-A7C5-4D83-A137-90D6C3E68A00}
91+
{DE3AA974-14C3-402F-93F4-A4A5D3DC0131} = {74B81852-D3A9-49BA-A62F-A653FBB36665}
7192
EndGlobalSection
7293
EndGlobal

README.md

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Nuget](https://img.shields.io/nuget/dt/ModEndpoints)](https://www.nuget.org/packages/ModEndpoints/)
55
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/modabas/ModEndpoints/blob/main/LICENSE.txt)
66

7-
WebResultEndpoints and ServiceResultEndpoints organize ASP.NET Core Minimal Apis in REPR format endpoints and are integrated with [result](https://github.com/modabas/ModResults) pattern out of box.
7+
WebResultEndpoints and BusinessResultEndpoints organize ASP.NET Core Minimal Apis in REPR format endpoints and are integrated with [result](https://github.com/modabas/ModResults) pattern out of box.
88

99
# ModEndpoints.Core
1010

@@ -14,7 +14,7 @@ Also contains core classes for ModEndpoints project.
1414

1515
## Introduction
1616

17-
The WebResultEndpoint and ServiceResultEndpoint abstractions are a structured approach to defining endpoints in ASP.NET Core applications. It extends the Minimal Api pattern with reusable, testable, and consistent components for request handling, validation, and response mapping.
17+
The WebResultEndpoint and BusinessResultEndpoint abstractions are a structured approach to defining endpoints in ASP.NET Core applications. It extends the Minimal Api pattern with reusable, testable, and consistent components for request handling, validation, and response mapping.
1818

1919
## Key Features
2020

@@ -280,7 +280,7 @@ See [test results](./samples/BenchmarkWebApi/BenchmarkFiles/inprocess_benchmark_
280280

281281
## Endpoint Types
282282

283-
WebResultEndpoint and ServiceResultEndpoint, the two abstract endpoint bases, have a 'HandleAsync' method which returns a strongly typed [business result](https://github.com/modabas/ModResults).
283+
WebResultEndpoint and BusinessResultEndpoint, the two abstract endpoint bases, have a 'HandleAsync' method which returns a strongly typed [business result](https://github.com/modabas/ModResults).
284284

285285
These two endpoint types differ only in converting these business results into HTTP responses before sending response to client.
286286

@@ -292,7 +292,7 @@ MinimalEndpoint within ModEndpoints.Core package, is closest to barebones Minima
292292

293293
Other features described previously are common for all of them.
294294

295-
Each type of andpoint has various implementations that accept a request model or not, that has a response model or not.
295+
Each type of endpoint has various implementations that accept a request model or not, that has a response model or not.
296296

297297
### MinimalEndpoint
298298

@@ -301,15 +301,6 @@ A MinimalEndpoint implementation, after handling request, returns the response m
301301
- MinimalEndpoint&lt;TRequest, TResponse&gt;: Has a request model, supports request validation and returns a response model.
302302
- MinimalEndpoint&lt;TResponse&gt;: Doesn't have a request model and returns a response model.
303303

304-
### ServiceResultEndpoint
305-
306-
A ServiceResultEndpoint implementation, after handling request, encapsulates the [business result](https://github.com/modabas/ModResults) of HandleAsync method in a HTTP 200 Minimal Api IResult and sends to client. The [business result](https://github.com/modabas/ModResults) returned may be in Ok or Failed state. This behaviour makes ServiceResultEndpoints more suitable for internal services, where clients are aware of Result or Result&lt;TValue&gt; implementations.
307-
308-
- ServiceResultEndpoint&lt;TRequest, TResultValue&gt;: Has a request model, supports request validation and returns a [Result&lt;TResultValue&gt;](https://github.com/modabas/ModResults) within HTTP 200 IResult.
309-
- ServiceResultEndpoint&lt;TRequest&gt;: Has a request model, supports request validation and returns a [Result](https://github.com/modabas/ModResults) within HTTP 200 IResult.
310-
- ServiceResultEndpointWithEmptyRequest&lt;TResultValue&gt;: Doesn't have a request model and returns a [Result&lt;TResultValue&gt;](https://github.com/modabas/ModResults) within HTTP 200 IResult.
311-
- ServiceResultEndpointWithEmptyRequest: Doesn't have a request model and returns a [Result](https://github.com/modabas/ModResults) within HTTP 200 IResult.
312-
313304
### WebResultEndpoint
314305

315306
A WebResultEndpoint implementation, after handling request, maps the [business result](https://github.com/modabas/ModResults) of HandleAsync method to a Minimal Api IResult depending on the business result type, state and failure type (if any). Mapping behaviour can be modified or replaced with a custom one.
@@ -336,3 +327,75 @@ It is also possible to implement a custom response mapping behaviour for a WebRe
336327
- Create an IResultToResponseMapper implementation,
337328
- Add it to dependency injection service collection with a string key during app startup,
338329
- Apply ResultToResponseMapper attribute to endpoint classes that will be using custom mapper. Use service registration string key as Name property of attribute.
330+
331+
### BusinessResultEndpoint
332+
333+
A BusinessResultEndpoint implementation, after handling request, encapsulates the [business result](https://github.com/modabas/ModResults) of HandleAsync method in a HTTP 200 Minimal Api IResult and sends to client. The [business result](https://github.com/modabas/ModResults) returned may be in Ok or Failed state. This behaviour makes BusinessResultEndpoints more suitable for internal services, where clients are aware of Result or Result&lt;TValue&gt; implementations.
334+
335+
- BusinessResultEndpoint&lt;TRequest, TResultValue&gt;: Has a request model, supports request validation and returns a [Result&lt;TResultValue&gt;](https://github.com/modabas/ModResults) within HTTP 200 IResult.
336+
- BusinessResultEndpoint&lt;TRequest&gt;: Has a request model, supports request validation and returns a [Result](https://github.com/modabas/ModResults) within HTTP 200 IResult.
337+
- BusinessResultEndpointWithEmptyRequest&lt;TResultValue&gt;: Doesn't have a request model and returns a [Result&lt;TResultValue&gt;](https://github.com/modabas/ModResults) within HTTP 200 IResult.
338+
- BusinessResultEndpointWithEmptyRequest: Doesn't have a request model and returns a [Result](https://github.com/modabas/ModResults) within HTTP 200 IResult.
339+
340+
341+
### ServiceEndpoint
342+
343+
This is a very specialized endpoint suitable for internal services. A ServiceEndpoint implementation, similar to BusinessResultEntpoint, encapsulates the response [business result](https://github.com/modabas/ModResults) of HandleAsync method in a HTTP 200 Minimal Api IResult and sends to client. The [business result](https://github.com/modabas/ModResults) returned may be in Ok or Failed state.
344+
345+
- ServiceEndpoint&lt;TRequest, TResultValue&gt;: Has a request model, supports request validation and returns a [Result&lt;TResultValue&gt;](https://github.com/modabas/ModResults) within HTTP 200 IResult.
346+
- ServiceEndpoint&lt;TRequest&gt;: Has a request model, supports request validation and returns a [Result](https://github.com/modabas/ModResults) within HTTP 200 IResult.
347+
348+
A ServiceEndpoint has following special traits and constraints:
349+
- A ServiceEndpoint is always registered with HttpMethod.Post method, and its bound pattern is determined accourding to its request type.
350+
- A ServiceEndpoint's request must implement either IServiceRequest (for ServiceEndpoint&lt;TRequest&gt;) or IServiceRequest&lt;TResultValue&gt; (for ServiceEndpoint&lt;TRequest, TResultValue&gt;)
351+
- A ServiceEndpoint's request is specific to that endpoint. Each endpoint must have its unique request type.
352+
- To utilize the advantages of a ServiceEndpoint over other endpoint types, its request and response types has to be shared with clients and therefore has to be in a seperate class library.
353+
354+
These enable clients to call ServiceEndpoints by a specialized message channel resolved from dependency injection, which has to be registered at client application startup with only service base address and service request type information. No other knowledge about service or client implementation is required.
355+
356+
Have a look at [sample ServiceEndpoint implementations](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi/Features/StoresWithServiceEndpoints) along with [sample client implementation](https://github.com/modabas/ModEndpoints/tree/main/samples/Client) and [request/response model shared library](https://github.com/modabas/ModEndpoints/tree/main/samples/ShowcaseWebApi.FeatureContracts).
357+
358+
A client has to register remote services from requests in an assembly during application startup, which utilizes IHttpClientFactory and HttpClient underneath and can be configured similarly...
359+
```csharp
360+
var baseAddress = "https://...";
361+
var clientName = "MyClient";
362+
builder.Services.AddRemoteServicesWithNewClient(
363+
typeof(ListStoresRequest).Assembly,
364+
clientName,
365+
(sp, client) =>
366+
{
367+
client.BaseAddress = new Uri(baseAddress);
368+
client.Timeout = TimeSpan.FromSeconds(5);
369+
})?.AddTransientHttpErrorPolicy(
370+
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
371+
```
372+
373+
or alternatively, register remote services one by one...
374+
```csharp
375+
var baseAddress = "https://...";
376+
var clientName = "MyClient";
377+
builder.Services.AddRemoteServiceWithNewClient<ListStoresRequest>(clientName,
378+
(sp, client) =>
379+
{
380+
client.BaseAddress = new Uri(baseAddress);
381+
client.Timeout = TimeSpan.FromSeconds(5);
382+
})
383+
.AddTransientHttpErrorPolicy(
384+
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
385+
builder.Services.AddRemoteServiceToExistingClient<GetStoreByIdRequest>(clientName);
386+
builder.Services.AddRemoteServiceToExistingClient<DeleteStoreRequest>(clientName);
387+
builder.Services.AddRemoteServiceToExistingClient<CreateStoreRequest>(clientName);
388+
builder.Services.AddRemoteServiceToExistingClient<UpdateStoreRequest>(clientName);
389+
```
390+
391+
Then call remote services with IServiceChannel instance resolved from DI...
392+
```csharp
393+
using IServiceScope serviceScope = hostProvider.CreateScope();
394+
IServiceProvider provider = serviceScope.ServiceProvider;
395+
396+
//resolve service channel from DI
397+
var channel = provider.GetRequiredService<IServiceChannel>();
398+
//send request over channel to remote service
399+
var listResult = await channel.SendAsync<ListStoresRequest, ListStoresResponse>(new ListStoresRequest(), ct);
400+
401+
```

samples/Client/Client.csproj

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="ModResults" Version="0.2.0" />
9+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
10+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\ModEndpoints.RemoteServices\ModEndpoints.RemoteServices.csproj" />
15+
<ProjectReference Include="..\ShowcaseWebApi.FeatureContracts\ShowcaseWebApi.FeatureContracts.csproj" />
1016
</ItemGroup>
1117

1218
</Project>

samples/Client/ListStores.cs

Lines changed: 0 additions & 3 deletions
This file was deleted.

samples/Client/Program.cs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,76 @@
11
// See https://aka.ms/new-console-template for more information
22

3-
using Client;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using ModEndpoints.RemoteServices;
6+
using ModResults;
7+
using Polly;
8+
using ShowcaseWebApi.FeatureContracts.Features.StoresWithServiceEndpoint;
49

5-
using (var client = new HttpClient())
10+
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
11+
12+
var baseAddress = "https://localhost:7012/api/v1/storesWithServiceEndpoint/";
13+
var clientName = "ShowcaseApi.Client";
14+
//builder.Services.AddRemoteServiceWithNewClient<ListStoresRequest>(clientName,
15+
// (sp, client) =>
16+
// {
17+
// client.BaseAddress = new Uri(baseAddress);
18+
// client.Timeout = TimeSpan.FromSeconds(5);
19+
// })
20+
// .AddTransientHttpErrorPolicy(
21+
// policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
22+
//builder.Services.AddRemoteServiceToExistingClient<GetStoreByIdRequest>(clientName);
23+
//builder.Services.AddRemoteServiceToExistingClient<DeleteStoreRequest>(clientName);
24+
//builder.Services.AddRemoteServiceToExistingClient<CreateStoreRequest>(clientName);
25+
//builder.Services.AddRemoteServiceToExistingClient<UpdateStoreRequest>(clientName);
26+
builder.Services.AddRemoteServicesWithNewClient(
27+
typeof(ListStoresRequest).Assembly,
28+
clientName,
29+
(sp, client) =>
30+
{
31+
client.BaseAddress = new Uri(baseAddress);
32+
client.Timeout = TimeSpan.FromSeconds(5);
33+
})?.AddTransientHttpErrorPolicy(
34+
policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
35+
36+
using IHost host = builder.Build();
37+
38+
await CallRemoteServicesAsync(host.Services);
39+
40+
await host.RunAsync();
41+
42+
static async Task CallRemoteServicesAsync(IServiceProvider hostProvider)
643
{
7-
var response = await client.GetAsync("http://localhost:5077/api/v1/stores");
8-
var result = await response.DeserializeResultAsync<ListStoresResponse>(default);
9-
if (result.IsOk)
44+
using IServiceScope serviceScope = hostProvider.CreateScope();
45+
IServiceProvider provider = serviceScope.ServiceProvider;
46+
47+
//resolve service channel from DI
48+
var channel = provider.GetRequiredService<IServiceChannel>();
49+
//send request over channel to remote service
50+
var listResult = await channel.SendAsync<ListStoresRequest, ListStoresResponse>(new ListStoresRequest(), default);
51+
52+
if (listResult.IsOk)
1053
{
11-
Console.WriteLine($"ListStores complete. Total count: {result.Value.Stores.Count}");
54+
Console.WriteLine($"ListStores complete. Total count: {listResult.Value.Stores.Count}");
55+
var id = listResult.Value.Stores.FirstOrDefault()?.Id;
56+
if (id is not null)
57+
{
58+
//send request over channel to remote service
59+
var getResult = await channel.SendAsync<GetStoreByIdRequest, GetStoreByIdResponse>(new GetStoreByIdRequest(Id: id.Value), default);
60+
if (getResult.IsOk)
61+
{
62+
Console.WriteLine(getResult.Value);
63+
}
64+
else
65+
{
66+
Console.WriteLine(getResult.DumpMessages());
67+
}
68+
}
1269
}
1370
else
1471
{
15-
if (result.Failure.Errors.Count > 0)
16-
{
17-
Console.WriteLine(string.Join(Environment.NewLine, result.Failure.Errors.Select(e => e.Message)));
18-
}
19-
else
20-
{
21-
Console.WriteLine($"ListStores failed.");
22-
}
72+
Console.WriteLine(listResult.DumpMessages());
2373
}
74+
75+
Console.WriteLine();
2476
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
using ModEndpoints.RemoteServices.Core;
2+
3+
namespace ShowcaseWebApi.FeatureContracts.Features.StoresWithServiceEndpoint;
4+
5+
public record CreateStoreRequest(string Name) : IServiceRequest<CreateStoreResponse>;
6+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace ShowcaseWebApi.FeatureContracts.Features.StoresWithServiceEndpoint;
2+
3+
public record CreateStoreResponse(Guid Id);
4+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using ModEndpoints.RemoteServices.Core;
2+
3+
namespace ShowcaseWebApi.FeatureContracts.Features.StoresWithServiceEndpoint;
4+
5+
public record DeleteStoreRequest(Guid Id) : IServiceRequest;

0 commit comments

Comments
 (0)