Skip to content

Commit d9f9e83

Browse files
authored
Dev (#9)
* - update modresults deps to latest - update readme for ModEndpoints.Core package * - add support to use different serializers from default service channel implementation * - decouple ServiceChannel from System.Text.Json * - bugfix: ServiceChannelSerializer not utilizing null DeserializationOptions * - make created http request message in service channel customizable via action input parameter - remove redundant input parameters * - cleanup code * - make http request customization delegate awaitable/async with additional cancellation token input * - default service channel implementation now sends http request with ContentsRead option - service channel send methods now accept an additional func parameter to process http response message received from remote endpoint and executes that function before deserialization starts - rename various classes * - code cleanup * - update deps to latest - bump version
1 parent d413a10 commit d9f9e83

17 files changed

+168
-91
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.5.2</Version>
21+
<Version>0.6.0</Version>
2222
</PropertyGroup>
2323
</Project>

samples/Client/Client.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
10-
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.1" />
9+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
10+
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.2" />
1111
</ItemGroup>
1212

1313
<ItemGroup>

src/ModEndpoints.Core/ModEndpoints.Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
<ItemGroup>
2424
<PackageReference Include="FluentValidation" Version="11.11.0" />
25-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
25+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
2626
</ItemGroup>
2727

2828
<ItemGroup>
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# ModEndpoints.Core
22

3-
MinimalEndpoints is the barebone implementation for organizing ASP.NET Core Minimal Apis in REPR format endpoints. Does not come integrated with a result pattern like endpoints in ModEndpoints project.
3+
[MinimalEndpoints](#minimalendpoint) are the barebone implementation for organizing ASP.NET Core Minimal Apis in REPR format endpoints. They have built-in input validation and their handler methods may return Minimal Api IResult based, string or T (any other type) response.
44

55
Also contains core classes for ModEndpoints project.
6+
7+
## Key Features
8+
9+
- Organizes ASP.NET Core Minimal Apis in REPR pattern endpoints
10+
- Encapsulates endpoint behaviors like request validation and request handling.
11+
- Supports anything that Minimal Apis does. Configuration, parameter binding, authentication, Open Api tooling, filters, etc. are all Minimal Apis under the hood.
12+
- Supports auto discovery and registration.
13+
- Has built-in validation support with [FluentValidation](https://github.com/FluentValidation/FluentValidation). If a validator is registered for request model, request is automatically validated before being handled.
14+
- Supports constructor dependency injection in endpoint implementations.
15+

src/ModEndpoints.RemoteServices/ServiceChannel.cs renamed to src/ModEndpoints.RemoteServices/DefaultServiceChannel.cs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
using System.Net.Http.Headers;
2-
using System.Net.Http.Json;
3-
using System.Text.Json;
4-
using Microsoft.Extensions.DependencyInjection;
1+
using Microsoft.Extensions.DependencyInjection;
52
using ModEndpoints.RemoteServices.Core;
63
using ModResults;
74

85
namespace ModEndpoints.RemoteServices;
96

10-
public class ServiceChannel(
7+
public class DefaultServiceChannel(
118
IHttpClientFactory clientFactory,
129
IServiceProvider serviceProvider)
1310
: IServiceChannel
@@ -18,10 +15,10 @@ public async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
1815
TRequest req,
1916
CancellationToken ct,
2017
string? endpointUriPrefix = null,
21-
MediaTypeHeaderValue? mediaType = null,
22-
JsonSerializerOptions? jsonSerializerOptions = null,
23-
Action<HttpRequestHeaders>? configureRequestHeaders = null,
24-
string? uriResolverName = null)
18+
Func<IServiceProvider, HttpRequestMessage, CancellationToken, Task>? processHttpRequest = null,
19+
Func<IServiceProvider, HttpResponseMessage, CancellationToken, Task>? processHttpResponse = null,
20+
string? uriResolverName = null,
21+
string? serializerName = null)
2522
where TRequest : IServiceRequest<TResponse>
2623
where TResponse : notnull
2724
{
@@ -31,6 +28,8 @@ public async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
3128
{
3229
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService<IServiceEndpointUriResolver>(
3330
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
31+
var serializer = scope.ServiceProvider.GetRequiredKeyedService<IServiceChannelSerializer>(
32+
serializerName ?? ServiceEndpointDefinitions.DefaultSerializerName);
3433
var requestUriResult = uriResolver.Resolve(req);
3534
if (requestUriResult.IsFailed)
3635
{
@@ -42,14 +41,21 @@ public async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
4241
}
4342
using (HttpRequestMessage httpReq = new(
4443
HttpMethod.Post,
45-
ServiceChannel.Combine(endpointUriPrefix, requestUriResult.Value)))
44+
Combine(endpointUriPrefix, requestUriResult.Value)))
4645
{
47-
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
48-
configureRequestHeaders?.Invoke(httpReq.Headers);
46+
httpReq.Content = await serializer.CreateContentAsync(req, ct);
47+
if (processHttpRequest is not null)
48+
{
49+
await processHttpRequest(scope.ServiceProvider, httpReq, ct);
50+
}
4951
var client = clientFactory.CreateClient(clientName);
50-
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
52+
using (var httpResponse = await client.SendAsync(httpReq, ct))
5153
{
52-
return await httpResponse.DeserializeResultAsync<TResponse>(ct);
54+
if (processHttpResponse is not null)
55+
{
56+
await processHttpResponse(scope.ServiceProvider, httpResponse, ct);
57+
}
58+
return await serializer.DeserializeResultAsync<TResponse>(httpResponse, ct);
5359
}
5460
}
5561
}
@@ -64,10 +70,10 @@ public async Task<Result> SendAsync<TRequest>(
6470
TRequest req,
6571
CancellationToken ct,
6672
string? endpointUriPrefix = null,
67-
MediaTypeHeaderValue? mediaType = null,
68-
JsonSerializerOptions? jsonSerializerOptions = null,
69-
Action<HttpRequestHeaders>? configureRequestHeaders = null,
70-
string? uriResolverName = null)
73+
Func<IServiceProvider, HttpRequestMessage, CancellationToken, Task>? processHttpRequest = null,
74+
Func<IServiceProvider, HttpResponseMessage, CancellationToken, Task>? processHttpResponse = null,
75+
string? uriResolverName = null,
76+
string? serializerName = null)
7177
where TRequest : IServiceRequest
7278
{
7379
try
@@ -76,6 +82,8 @@ public async Task<Result> SendAsync<TRequest>(
7682
{
7783
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService<IServiceEndpointUriResolver>(
7884
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
85+
var serializer = scope.ServiceProvider.GetRequiredKeyedService<IServiceChannelSerializer>(
86+
serializerName ?? ServiceEndpointDefinitions.DefaultSerializerName);
7987
var requestUriResult = uriResolver.Resolve(req);
8088
if (requestUriResult.IsFailed)
8189
{
@@ -87,14 +95,21 @@ public async Task<Result> SendAsync<TRequest>(
8795
}
8896
using (HttpRequestMessage httpReq = new(
8997
HttpMethod.Post,
90-
ServiceChannel.Combine(endpointUriPrefix, requestUriResult.Value)))
98+
Combine(endpointUriPrefix, requestUriResult.Value)))
9199
{
92-
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
93-
configureRequestHeaders?.Invoke(httpReq.Headers);
100+
httpReq.Content = await serializer.CreateContentAsync(req, ct);
101+
if (processHttpRequest is not null)
102+
{
103+
await processHttpRequest(scope.ServiceProvider, httpReq, ct);
104+
}
94105
var client = clientFactory.CreateClient(clientName);
95-
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
106+
using (var httpResponse = await client.SendAsync(httpReq, ct))
96107
{
97-
return await httpResponse.DeserializeResultAsync(ct);
108+
if (processHttpResponse is not null)
109+
{
110+
await processHttpResponse(scope.ServiceProvider, httpResponse, ct);
111+
}
112+
return await serializer.DeserializeResultAsync(httpResponse, ct);
98113
}
99114
}
100115
}

src/ModEndpoints.RemoteServices/HttpResponseMessageExtensions.cs renamed to src/ModEndpoints.RemoteServices/DefaultServiceChannelSerializer.cs

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
using System.Text.Json;
2-
using System.Text.Json.Serialization;
1+
using System.Net.Http.Json;
2+
using System.Text.Json;
3+
using ModEndpoints.RemoteServices.Core;
34
using ModResults;
45

56
namespace ModEndpoints.RemoteServices;
67

7-
public static class HttpResponseMessageExtensions
8+
public class DefaultServiceChannelSerializer(
9+
ServiceChannelSerializerOptions options)
10+
: IServiceChannelSerializer
811
{
9-
private static readonly JsonSerializerOptions _defaultJsonSerializerOptions = new()
10-
{
11-
PropertyNameCaseInsensitive = true,
12-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
13-
NumberHandling = JsonNumberHandling.AllowReadingFromString
14-
};
15-
1612
private const string DeserializationErrorMessage =
1713
"Cannot deserialize Result object from http response message.";
1814

@@ -25,15 +21,26 @@ public static class HttpResponseMessageExtensions
2521
private const string InstanceFactMessage =
2622
"Instance: {0} {1}";
2723

28-
public static async Task<Result<T>> DeserializeResultAsync<T>(
29-
this HttpResponseMessage response,
30-
CancellationToken ct,
31-
JsonSerializerOptions? jsonSerializerOptions = null)
32-
where T : notnull
24+
public ValueTask<HttpContent> CreateContentAsync<TRequest>(
25+
TRequest request,
26+
CancellationToken ct)
27+
where TRequest : IServiceRequestMarker
28+
{
29+
return new ValueTask<HttpContent>(
30+
JsonContent.Create(
31+
request,
32+
null,
33+
options.SerializationOptions));
34+
}
35+
36+
public async Task<Result<TResponse>> DeserializeResultAsync<TResponse>(
37+
HttpResponseMessage response,
38+
CancellationToken ct)
39+
where TResponse : notnull
3340
{
3441
if (!response.IsSuccessStatusCode)
3542
{
36-
return Result<T>
43+
return Result<TResponse>
3744
.CriticalError(string.Format(
3845
string.IsNullOrWhiteSpace(response.ReasonPhrase) ? ResponseNotSuccessfulErrorMessage : ResponseNotSuccessfulWithReasonErrorMessage,
3946
(int)response.StatusCode,
@@ -43,19 +50,18 @@ public static async Task<Result<T>> DeserializeResultAsync<T>(
4350
response.RequestMessage?.Method,
4451
response.RequestMessage?.RequestUri));
4552
}
46-
var resultObject = await response.DeserializeResultInternalAsync<Result<T>>(jsonSerializerOptions, ct);
47-
return resultObject ?? Result<T>
53+
var resultObject = await DeserializeResultInternalAsync<Result<TResponse>>(response, ct);
54+
return resultObject ?? Result<TResponse>
4855
.CriticalError(DeserializationErrorMessage)
4956
.WithFact(string.Format(
5057
InstanceFactMessage,
5158
response.RequestMessage?.Method,
5259
response.RequestMessage?.RequestUri));
5360
}
5461

55-
public static async Task<Result> DeserializeResultAsync(
56-
this HttpResponseMessage response,
57-
CancellationToken ct,
58-
JsonSerializerOptions? jsonSerializerOptions = null)
62+
public async Task<Result> DeserializeResultAsync(
63+
HttpResponseMessage response,
64+
CancellationToken ct)
5965
{
6066
if (!response.IsSuccessStatusCode)
6167
{
@@ -69,7 +75,7 @@ public static async Task<Result> DeserializeResultAsync(
6975
response.RequestMessage?.Method,
7076
response.RequestMessage?.RequestUri));
7177
}
72-
var resultObject = await response.DeserializeResultInternalAsync<Result>(jsonSerializerOptions, ct);
78+
var resultObject = await DeserializeResultInternalAsync<Result>(response, ct);
7379
return resultObject ?? Result
7480
.CriticalError(DeserializationErrorMessage)
7581
.WithFact(string.Format(
@@ -78,9 +84,8 @@ public static async Task<Result> DeserializeResultAsync(
7884
response.RequestMessage?.RequestUri));
7985
}
8086

81-
private static async Task<TResult?> DeserializeResultInternalAsync<TResult>(
82-
this HttpResponseMessage response,
83-
JsonSerializerOptions? jsonSerializerOptions,
87+
private async Task<TResult?> DeserializeResultInternalAsync<TResult>(
88+
HttpResponseMessage response,
8489
CancellationToken ct)
8590
where TResult : IModResult
8691
{
@@ -91,7 +96,7 @@ public static async Task<Result> DeserializeResultAsync(
9196
ct.ThrowIfCancellationRequested();
9297
return await JsonSerializer.DeserializeAsync<TResult>(
9398
contentStream,
94-
jsonSerializerOptions ?? _defaultJsonSerializerOptions,
99+
options.DeserializationOptions,
95100
ct);
96101
}
97102
}

src/ModEndpoints.RemoteServices/ServiceEndpointUriResolver.cs renamed to src/ModEndpoints.RemoteServices/DefaultServiceEndpointUriResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using ModResults;
33

44
namespace ModEndpoints.RemoteServices;
5-
public class ServiceEndpointUriResolver : IServiceEndpointUriResolver
5+
public class DefaultServiceEndpointUriResolver : IServiceEndpointUriResolver
66
{
77
private const string CannotResolveServiceEndpointUri = "Cannot resolve request uri for service endpoint.";
88
public Result<string> Resolve(IServiceRequestMarker req)

src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static IServiceCollection AddRemoteServiceWithNewClient<TRequest>(
2727
Action<IHttpClientBuilder>? configureClientBuilder = null)
2828
where TRequest : IServiceRequestMarker
2929
{
30-
var clientName = DefaultClientName.Resolve<TRequest>();
30+
var clientName = ServiceClientNameResolver.GetDefaultName<TRequest>();
3131
return services.AddRemoteServiceWithNewClient<TRequest>(
3232
clientName,
3333
baseAddress,
@@ -81,7 +81,7 @@ public static IServiceCollection AddRemoteServiceWithNewClient<TRequest>(
8181
Action<IHttpClientBuilder>? configureClientBuilder = null)
8282
where TRequest : IServiceRequestMarker
8383
{
84-
var clientName = DefaultClientName.Resolve<TRequest>();
84+
var clientName = ServiceClientNameResolver.GetDefaultName<TRequest>();
8585
return services.AddRemoteServiceWithNewClient<TRequest>(
8686
clientName,
8787
configureClient,
@@ -277,9 +277,19 @@ private static IServiceCollection AddClientInternal(
277277
private static IServiceCollection AddRemoteServicesCore(
278278
this IServiceCollection services)
279279
{
280-
services.TryAddKeyedSingleton<IServiceEndpointUriResolver, ServiceEndpointUriResolver>(
280+
services.TryAddKeyedSingleton<IServiceEndpointUriResolver, DefaultServiceEndpointUriResolver>(
281281
ServiceEndpointDefinitions.DefaultUriResolverName);
282-
services.TryAddTransient<IServiceChannel, ServiceChannel>();
282+
services.AddKeyedTransient<IServiceChannelSerializer, DefaultServiceChannelSerializer>(
283+
ServiceEndpointDefinitions.DefaultSerializerName,
284+
(_, _) =>
285+
{
286+
return new DefaultServiceChannelSerializer(new ServiceChannelSerializerOptions()
287+
{
288+
SerializationOptions = null,
289+
DeserializationOptions = ServiceEndpointDefinitions.DefaultJsonDeserializationOptions
290+
});
291+
});
292+
services.TryAddTransient<IServiceChannel, DefaultServiceChannel>();
283293
return services;
284294
}
285295

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Net.Http.Headers;
2-
using System.Text.Json;
3-
using ModEndpoints.RemoteServices.Core;
1+
using ModEndpoints.RemoteServices.Core;
42
using ModResults;
53

64
namespace ModEndpoints.RemoteServices;
@@ -18,19 +16,19 @@ public interface IServiceChannel
1816
/// <param name="req">Request to be sent.</param>
1917
/// <param name="ct">The <see cref="CancellationToken"/> to cancel operation.</param>
2018
/// <param name="endpointUriPrefix">Path to append as prefix to resolved enpoint uri. Usually used to add path segments to configured client's base address.</param>
21-
/// <param name="mediaType">The media type to use for the content.</param>
22-
/// <param name="jsonSerializerOptions">Options to control the behavior during serialization.</param>
23-
/// <param name="configureRequestHeaders">Delegate to configure HTTP request headers.</param>
19+
/// <param name="processHttpRequest">Delegate to further configure created HTTP request message (headers, etc) before sending to ServiceEndpoint.</param>
20+
/// <param name="processHttpResponse">Delegate to process received HTTP response message of ServiceEndpoint before deserialization.</param>
2421
/// <param name="uriResolverName"><see cref="IServiceEndpointUriResolver"/> name to be used to resolve ServiceEnpoint Uri.</param>
22+
/// <param name="serializerName"><see cref="IServiceChannelSerializer"/> name to be used to resolve ServiceEnpoint Uri.</param>
2523
/// <returns>Response of remote service endpoint or failure result.</returns>
2624
Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
2725
TRequest req,
2826
CancellationToken ct,
2927
string? endpointUriPrefix = null,
30-
MediaTypeHeaderValue? mediaType = null,
31-
JsonSerializerOptions? jsonSerializerOptions = null,
32-
Action<HttpRequestHeaders>? configureRequestHeaders = null,
33-
string? uriResolverName = null)
28+
Func<IServiceProvider, HttpRequestMessage, CancellationToken, Task>? processHttpRequest = null,
29+
Func<IServiceProvider, HttpResponseMessage, CancellationToken, Task>? processHttpResponse = null,
30+
string? uriResolverName = null,
31+
string? serializerName = null)
3432
where TRequest : IServiceRequest<TResponse>
3533
where TResponse : notnull;
3634

@@ -41,18 +39,18 @@ Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
4139
/// <param name="req">Request to be sent.</param>
4240
/// <param name="ct">The <see cref="CancellationToken"/> to cancel operation.</param>
4341
/// <param name="endpointUriPrefix">Path to append as prefix to resolved enpoint uri. Usually used to add path segments to configured client's base address.</param>
44-
/// <param name="mediaType">The media type to use for the content.</param>
45-
/// <param name="jsonSerializerOptions">Options to control the behavior during serialization.</param>
46-
/// <param name="configureRequestHeaders">Delegate to configure HTTP request headers.</param>
42+
/// <param name="processHttpRequest">Delegate to further configure created HTTP request message (headers, etc) before sending to ServiceEndpoint.</param>
43+
/// <param name="processHttpResponse">Delegate to process received HTTP response message of ServiceEndpoint before deserialization.</param>
4744
/// <param name="uriResolverName"><see cref="IServiceEndpointUriResolver"/> name to be used to resolve ServiceEnpoint Uri.</param>
45+
/// <param name="serializerName"><see cref="IServiceChannelSerializer"/> name to be used to resolve ServiceEnpoint Uri.</param>
4846
/// <returns>Response of remote service endpoint or failure result.</returns>
4947
Task<Result> SendAsync<TRequest>(
5048
TRequest req,
5149
CancellationToken ct,
5250
string? endpointUriPrefix = null,
53-
MediaTypeHeaderValue? mediaType = null,
54-
JsonSerializerOptions? jsonSerializerOptions = null,
55-
Action<HttpRequestHeaders>? configureRequestHeaders = null,
56-
string? uriResolverName = null)
51+
Func<IServiceProvider, HttpRequestMessage, CancellationToken, Task>? processHttpRequest = null,
52+
Func<IServiceProvider, HttpResponseMessage, CancellationToken, Task>? processHttpResponse = null,
53+
string? uriResolverName = null,
54+
string? serializerName = null)
5755
where TRequest : IServiceRequest;
5856
}

0 commit comments

Comments
 (0)