diff --git a/Directory.Build.props b/Directory.Build.props
index 8a388ee..17dd053 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,6 +18,6 @@
MIT
True
- 0.5.2
+ 0.6.0
\ No newline at end of file
diff --git a/samples/Client/Client.csproj b/samples/Client/Client.csproj
index ff828af..59f10ef 100644
--- a/samples/Client/Client.csproj
+++ b/samples/Client/Client.csproj
@@ -6,8 +6,8 @@
-
-
+
+
diff --git a/src/ModEndpoints.Core/ModEndpoints.Core.csproj b/src/ModEndpoints.Core/ModEndpoints.Core.csproj
index 6e5c36b..429b4a8 100644
--- a/src/ModEndpoints.Core/ModEndpoints.Core.csproj
+++ b/src/ModEndpoints.Core/ModEndpoints.Core.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/src/ModEndpoints.Core/assets/README.md b/src/ModEndpoints.Core/assets/README.md
index 1481f38..911c177 100644
--- a/src/ModEndpoints.Core/assets/README.md
+++ b/src/ModEndpoints.Core/assets/README.md
@@ -1,5 +1,15 @@
# ModEndpoints.Core
-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.
+[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.
Also contains core classes for ModEndpoints project.
+
+## Key Features
+
+ - Organizes ASP.NET Core Minimal Apis in REPR pattern endpoints
+ - Encapsulates endpoint behaviors like request validation and request handling.
+ - Supports anything that Minimal Apis does. Configuration, parameter binding, authentication, Open Api tooling, filters, etc. are all Minimal Apis under the hood.
+ - Supports auto discovery and registration.
+ - 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.
+ - Supports constructor dependency injection in endpoint implementations.
+
\ No newline at end of file
diff --git a/src/ModEndpoints.RemoteServices/ServiceChannel.cs b/src/ModEndpoints.RemoteServices/DefaultServiceChannel.cs
similarity index 59%
rename from src/ModEndpoints.RemoteServices/ServiceChannel.cs
rename to src/ModEndpoints.RemoteServices/DefaultServiceChannel.cs
index e39140c..82fc44e 100644
--- a/src/ModEndpoints.RemoteServices/ServiceChannel.cs
+++ b/src/ModEndpoints.RemoteServices/DefaultServiceChannel.cs
@@ -1,13 +1,10 @@
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using System.Text.Json;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using ModEndpoints.RemoteServices.Core;
using ModResults;
namespace ModEndpoints.RemoteServices;
-public class ServiceChannel(
+public class DefaultServiceChannel(
IHttpClientFactory clientFactory,
IServiceProvider serviceProvider)
: IServiceChannel
@@ -18,10 +15,10 @@ public async Task> SendAsync(
TRequest req,
CancellationToken ct,
string? endpointUriPrefix = null,
- MediaTypeHeaderValue? mediaType = null,
- JsonSerializerOptions? jsonSerializerOptions = null,
- Action? configureRequestHeaders = null,
- string? uriResolverName = null)
+ Func? processHttpRequest = null,
+ Func? processHttpResponse = null,
+ string? uriResolverName = null,
+ string? serializerName = null)
where TRequest : IServiceRequest
where TResponse : notnull
{
@@ -31,6 +28,8 @@ public async Task> SendAsync(
{
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService(
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
+ var serializer = scope.ServiceProvider.GetRequiredKeyedService(
+ serializerName ?? ServiceEndpointDefinitions.DefaultSerializerName);
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
{
@@ -42,14 +41,21 @@ public async Task> SendAsync(
}
using (HttpRequestMessage httpReq = new(
HttpMethod.Post,
- ServiceChannel.Combine(endpointUriPrefix, requestUriResult.Value)))
+ Combine(endpointUriPrefix, requestUriResult.Value)))
{
- httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
- configureRequestHeaders?.Invoke(httpReq.Headers);
+ httpReq.Content = await serializer.CreateContentAsync(req, ct);
+ if (processHttpRequest is not null)
+ {
+ await processHttpRequest(scope.ServiceProvider, httpReq, ct);
+ }
var client = clientFactory.CreateClient(clientName);
- using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
+ using (var httpResponse = await client.SendAsync(httpReq, ct))
{
- return await httpResponse.DeserializeResultAsync(ct);
+ if (processHttpResponse is not null)
+ {
+ await processHttpResponse(scope.ServiceProvider, httpResponse, ct);
+ }
+ return await serializer.DeserializeResultAsync(httpResponse, ct);
}
}
}
@@ -64,10 +70,10 @@ public async Task SendAsync(
TRequest req,
CancellationToken ct,
string? endpointUriPrefix = null,
- MediaTypeHeaderValue? mediaType = null,
- JsonSerializerOptions? jsonSerializerOptions = null,
- Action? configureRequestHeaders = null,
- string? uriResolverName = null)
+ Func? processHttpRequest = null,
+ Func? processHttpResponse = null,
+ string? uriResolverName = null,
+ string? serializerName = null)
where TRequest : IServiceRequest
{
try
@@ -76,6 +82,8 @@ public async Task SendAsync(
{
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService(
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
+ var serializer = scope.ServiceProvider.GetRequiredKeyedService(
+ serializerName ?? ServiceEndpointDefinitions.DefaultSerializerName);
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
{
@@ -87,14 +95,21 @@ public async Task SendAsync(
}
using (HttpRequestMessage httpReq = new(
HttpMethod.Post,
- ServiceChannel.Combine(endpointUriPrefix, requestUriResult.Value)))
+ Combine(endpointUriPrefix, requestUriResult.Value)))
{
- httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
- configureRequestHeaders?.Invoke(httpReq.Headers);
+ httpReq.Content = await serializer.CreateContentAsync(req, ct);
+ if (processHttpRequest is not null)
+ {
+ await processHttpRequest(scope.ServiceProvider, httpReq, ct);
+ }
var client = clientFactory.CreateClient(clientName);
- using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
+ using (var httpResponse = await client.SendAsync(httpReq, ct))
{
- return await httpResponse.DeserializeResultAsync(ct);
+ if (processHttpResponse is not null)
+ {
+ await processHttpResponse(scope.ServiceProvider, httpResponse, ct);
+ }
+ return await serializer.DeserializeResultAsync(httpResponse, ct);
}
}
}
diff --git a/src/ModEndpoints.RemoteServices/HttpResponseMessageExtensions.cs b/src/ModEndpoints.RemoteServices/DefaultServiceChannelSerializer.cs
similarity index 64%
rename from src/ModEndpoints.RemoteServices/HttpResponseMessageExtensions.cs
rename to src/ModEndpoints.RemoteServices/DefaultServiceChannelSerializer.cs
index f936362..90992c2 100644
--- a/src/ModEndpoints.RemoteServices/HttpResponseMessageExtensions.cs
+++ b/src/ModEndpoints.RemoteServices/DefaultServiceChannelSerializer.cs
@@ -1,18 +1,14 @@
-using System.Text.Json;
-using System.Text.Json.Serialization;
+using System.Net.Http.Json;
+using System.Text.Json;
+using ModEndpoints.RemoteServices.Core;
using ModResults;
namespace ModEndpoints.RemoteServices;
-public static class HttpResponseMessageExtensions
+public class DefaultServiceChannelSerializer(
+ ServiceChannelSerializerOptions options)
+ : IServiceChannelSerializer
{
- private static readonly JsonSerializerOptions _defaultJsonSerializerOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- NumberHandling = JsonNumberHandling.AllowReadingFromString
- };
-
private const string DeserializationErrorMessage =
"Cannot deserialize Result object from http response message.";
@@ -25,15 +21,26 @@ public static class HttpResponseMessageExtensions
private const string InstanceFactMessage =
"Instance: {0} {1}";
- public static async Task> DeserializeResultAsync(
- this HttpResponseMessage response,
- CancellationToken ct,
- JsonSerializerOptions? jsonSerializerOptions = null)
- where T : notnull
+ public ValueTask CreateContentAsync(
+ TRequest request,
+ CancellationToken ct)
+ where TRequest : IServiceRequestMarker
+ {
+ return new ValueTask(
+ JsonContent.Create(
+ request,
+ null,
+ options.SerializationOptions));
+ }
+
+ public async Task> DeserializeResultAsync(
+ HttpResponseMessage response,
+ CancellationToken ct)
+ where TResponse : notnull
{
if (!response.IsSuccessStatusCode)
{
- return Result
+ return Result
.CriticalError(string.Format(
string.IsNullOrWhiteSpace(response.ReasonPhrase) ? ResponseNotSuccessfulErrorMessage : ResponseNotSuccessfulWithReasonErrorMessage,
(int)response.StatusCode,
@@ -43,8 +50,8 @@ public static async Task> DeserializeResultAsync(
response.RequestMessage?.Method,
response.RequestMessage?.RequestUri));
}
- var resultObject = await response.DeserializeResultInternalAsync>(jsonSerializerOptions, ct);
- return resultObject ?? Result
+ var resultObject = await DeserializeResultInternalAsync>(response, ct);
+ return resultObject ?? Result
.CriticalError(DeserializationErrorMessage)
.WithFact(string.Format(
InstanceFactMessage,
@@ -52,10 +59,9 @@ public static async Task> DeserializeResultAsync(
response.RequestMessage?.RequestUri));
}
- public static async Task DeserializeResultAsync(
- this HttpResponseMessage response,
- CancellationToken ct,
- JsonSerializerOptions? jsonSerializerOptions = null)
+ public async Task DeserializeResultAsync(
+ HttpResponseMessage response,
+ CancellationToken ct)
{
if (!response.IsSuccessStatusCode)
{
@@ -69,7 +75,7 @@ public static async Task DeserializeResultAsync(
response.RequestMessage?.Method,
response.RequestMessage?.RequestUri));
}
- var resultObject = await response.DeserializeResultInternalAsync(jsonSerializerOptions, ct);
+ var resultObject = await DeserializeResultInternalAsync(response, ct);
return resultObject ?? Result
.CriticalError(DeserializationErrorMessage)
.WithFact(string.Format(
@@ -78,9 +84,8 @@ public static async Task DeserializeResultAsync(
response.RequestMessage?.RequestUri));
}
- private static async Task DeserializeResultInternalAsync(
- this HttpResponseMessage response,
- JsonSerializerOptions? jsonSerializerOptions,
+ private async Task DeserializeResultInternalAsync(
+ HttpResponseMessage response,
CancellationToken ct)
where TResult : IModResult
{
@@ -91,7 +96,7 @@ public static async Task DeserializeResultAsync(
ct.ThrowIfCancellationRequested();
return await JsonSerializer.DeserializeAsync(
contentStream,
- jsonSerializerOptions ?? _defaultJsonSerializerOptions,
+ options.DeserializationOptions,
ct);
}
}
diff --git a/src/ModEndpoints.RemoteServices/ServiceEndpointUriResolver.cs b/src/ModEndpoints.RemoteServices/DefaultServiceEndpointUriResolver.cs
similarity index 91%
rename from src/ModEndpoints.RemoteServices/ServiceEndpointUriResolver.cs
rename to src/ModEndpoints.RemoteServices/DefaultServiceEndpointUriResolver.cs
index 126a64b..3bdca2e 100644
--- a/src/ModEndpoints.RemoteServices/ServiceEndpointUriResolver.cs
+++ b/src/ModEndpoints.RemoteServices/DefaultServiceEndpointUriResolver.cs
@@ -2,7 +2,7 @@
using ModResults;
namespace ModEndpoints.RemoteServices;
-public class ServiceEndpointUriResolver : IServiceEndpointUriResolver
+public class DefaultServiceEndpointUriResolver : IServiceEndpointUriResolver
{
private const string CannotResolveServiceEndpointUri = "Cannot resolve request uri for service endpoint.";
public Result Resolve(IServiceRequestMarker req)
diff --git a/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs b/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs
index cf907e6..2594edb 100644
--- a/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs
+++ b/src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs
@@ -27,7 +27,7 @@ public static IServiceCollection AddRemoteServiceWithNewClient(
Action? configureClientBuilder = null)
where TRequest : IServiceRequestMarker
{
- var clientName = DefaultClientName.Resolve();
+ var clientName = ServiceClientNameResolver.GetDefaultName();
return services.AddRemoteServiceWithNewClient(
clientName,
baseAddress,
@@ -81,7 +81,7 @@ public static IServiceCollection AddRemoteServiceWithNewClient(
Action? configureClientBuilder = null)
where TRequest : IServiceRequestMarker
{
- var clientName = DefaultClientName.Resolve();
+ var clientName = ServiceClientNameResolver.GetDefaultName();
return services.AddRemoteServiceWithNewClient(
clientName,
configureClient,
@@ -277,9 +277,19 @@ private static IServiceCollection AddClientInternal(
private static IServiceCollection AddRemoteServicesCore(
this IServiceCollection services)
{
- services.TryAddKeyedSingleton(
+ services.TryAddKeyedSingleton(
ServiceEndpointDefinitions.DefaultUriResolverName);
- services.TryAddTransient();
+ services.AddKeyedTransient(
+ ServiceEndpointDefinitions.DefaultSerializerName,
+ (_, _) =>
+ {
+ return new DefaultServiceChannelSerializer(new ServiceChannelSerializerOptions()
+ {
+ SerializationOptions = null,
+ DeserializationOptions = ServiceEndpointDefinitions.DefaultJsonDeserializationOptions
+ });
+ });
+ services.TryAddTransient();
return services;
}
diff --git a/src/ModEndpoints.RemoteServices/IServiceChannel.cs b/src/ModEndpoints.RemoteServices/IServiceChannel.cs
index 8c6b9c6..4758628 100644
--- a/src/ModEndpoints.RemoteServices/IServiceChannel.cs
+++ b/src/ModEndpoints.RemoteServices/IServiceChannel.cs
@@ -1,6 +1,4 @@
-using System.Net.Http.Headers;
-using System.Text.Json;
-using ModEndpoints.RemoteServices.Core;
+using ModEndpoints.RemoteServices.Core;
using ModResults;
namespace ModEndpoints.RemoteServices;
@@ -18,19 +16,19 @@ public interface IServiceChannel
/// Request to be sent.
/// The to cancel operation.
/// Path to append as prefix to resolved enpoint uri. Usually used to add path segments to configured client's base address.
- /// The media type to use for the content.
- /// Options to control the behavior during serialization.
- /// Delegate to configure HTTP request headers.
+ /// Delegate to further configure created HTTP request message (headers, etc) before sending to ServiceEndpoint.
+ /// Delegate to process received HTTP response message of ServiceEndpoint before deserialization.
/// name to be used to resolve ServiceEnpoint Uri.
+ /// name to be used to resolve ServiceEnpoint Uri.
/// Response of remote service endpoint or failure result.
Task> SendAsync(
TRequest req,
CancellationToken ct,
string? endpointUriPrefix = null,
- MediaTypeHeaderValue? mediaType = null,
- JsonSerializerOptions? jsonSerializerOptions = null,
- Action? configureRequestHeaders = null,
- string? uriResolverName = null)
+ Func? processHttpRequest = null,
+ Func? processHttpResponse = null,
+ string? uriResolverName = null,
+ string? serializerName = null)
where TRequest : IServiceRequest
where TResponse : notnull;
@@ -41,18 +39,18 @@ Task> SendAsync(
/// Request to be sent.
/// The to cancel operation.
/// Path to append as prefix to resolved enpoint uri. Usually used to add path segments to configured client's base address.
- /// The media type to use for the content.
- /// Options to control the behavior during serialization.
- /// Delegate to configure HTTP request headers.
+ /// Delegate to further configure created HTTP request message (headers, etc) before sending to ServiceEndpoint.
+ /// Delegate to process received HTTP response message of ServiceEndpoint before deserialization.
/// name to be used to resolve ServiceEnpoint Uri.
+ /// name to be used to resolve ServiceEnpoint Uri.
/// Response of remote service endpoint or failure result.
Task SendAsync(
TRequest req,
CancellationToken ct,
string? endpointUriPrefix = null,
- MediaTypeHeaderValue? mediaType = null,
- JsonSerializerOptions? jsonSerializerOptions = null,
- Action? configureRequestHeaders = null,
- string? uriResolverName = null)
+ Func? processHttpRequest = null,
+ Func? processHttpResponse = null,
+ string? uriResolverName = null,
+ string? serializerName = null)
where TRequest : IServiceRequest;
}
diff --git a/src/ModEndpoints.RemoteServices/IServiceChannelSerializer.cs b/src/ModEndpoints.RemoteServices/IServiceChannelSerializer.cs
new file mode 100644
index 0000000..13435e5
--- /dev/null
+++ b/src/ModEndpoints.RemoteServices/IServiceChannelSerializer.cs
@@ -0,0 +1,20 @@
+using ModEndpoints.RemoteServices.Core;
+using ModResults;
+
+namespace ModEndpoints.RemoteServices;
+public interface IServiceChannelSerializer
+{
+ Task DeserializeResultAsync(
+ HttpResponseMessage response,
+ CancellationToken ct);
+
+ Task> DeserializeResultAsync(
+ HttpResponseMessage response,
+ CancellationToken ct)
+ where TResponse : notnull;
+
+ ValueTask CreateContentAsync(
+ TRequest request,
+ CancellationToken ct)
+ where TRequest : IServiceRequestMarker;
+}
diff --git a/src/ModEndpoints.RemoteServices/ModEndpoints.RemoteServices.csproj b/src/ModEndpoints.RemoteServices/ModEndpoints.RemoteServices.csproj
index 1a9c9d1..090162f 100644
--- a/src/ModEndpoints.RemoteServices/ModEndpoints.RemoteServices.csproj
+++ b/src/ModEndpoints.RemoteServices/ModEndpoints.RemoteServices.csproj
@@ -21,8 +21,8 @@
-
-
+
+
diff --git a/src/ModEndpoints.RemoteServices/ServiceChannelSerializerOptions.cs b/src/ModEndpoints.RemoteServices/ServiceChannelSerializerOptions.cs
new file mode 100644
index 0000000..510ed7f
--- /dev/null
+++ b/src/ModEndpoints.RemoteServices/ServiceChannelSerializerOptions.cs
@@ -0,0 +1,8 @@
+using System.Text.Json;
+
+namespace ModEndpoints.RemoteServices;
+public class ServiceChannelSerializerOptions
+{
+ public JsonSerializerOptions? SerializationOptions { get; set; }
+ public JsonSerializerOptions? DeserializationOptions { get; set; }
+}
diff --git a/src/ModEndpoints.RemoteServices/DefaultClientName.cs b/src/ModEndpoints.RemoteServices/ServiceClientNameResolver.cs
similarity index 63%
rename from src/ModEndpoints.RemoteServices/DefaultClientName.cs
rename to src/ModEndpoints.RemoteServices/ServiceClientNameResolver.cs
index 187df78..87097ce 100644
--- a/src/ModEndpoints.RemoteServices/DefaultClientName.cs
+++ b/src/ModEndpoints.RemoteServices/ServiceClientNameResolver.cs
@@ -1,23 +1,23 @@
using ModEndpoints.RemoteServices.Core;
namespace ModEndpoints.RemoteServices;
-internal class DefaultClientName
+internal class ServiceClientNameResolver
{
private const string InvalidRequestType = "Request type should not be generic type parameter.";
- public static string Resolve(IServiceRequestMarker request)
+ public static string GetDefaultName(IServiceRequestMarker request)
{
var requestType = request.GetType();
- return ResolveInternal(requestType);
+ return GetDefaultNameInternal(requestType);
}
- public static string Resolve()
+ public static string GetDefaultName()
where TRequest : IServiceRequestMarker
{
var requestType = typeof(TRequest);
- return ResolveInternal(requestType);
+ return GetDefaultNameInternal(requestType);
}
- private static string ResolveInternal(Type requestType)
+ private static string GetDefaultNameInternal(Type requestType)
{
var requestName = requestType.AssemblyQualifiedName;
if (string.IsNullOrWhiteSpace(requestName))
diff --git a/src/ModEndpoints.RemoteServices/ServiceEndpointDefinitions.cs b/src/ModEndpoints.RemoteServices/ServiceEndpointDefinitions.cs
index 4967165..32ac097 100644
--- a/src/ModEndpoints.RemoteServices/ServiceEndpointDefinitions.cs
+++ b/src/ModEndpoints.RemoteServices/ServiceEndpointDefinitions.cs
@@ -1,5 +1,16 @@
-namespace ModEndpoints.RemoteServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ModEndpoints.RemoteServices;
public static class ServiceEndpointDefinitions
{
- public const string DefaultUriResolverName = "DefaultUriResolver";
+ public const string DefaultUriResolverName = "DefaultServiceEndpointUriResolver";
+ public const string DefaultSerializerName = "DefaultServiceChannelSerializer";
+
+ internal static readonly JsonSerializerOptions DefaultJsonDeserializationOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString
+ };
}
diff --git a/src/ModEndpoints/DependencyInjectionExtensions.cs b/src/ModEndpoints/DependencyInjectionExtensions.cs
index a1e0b60..29cc983 100644
--- a/src/ModEndpoints/DependencyInjectionExtensions.cs
+++ b/src/ModEndpoints/DependencyInjectionExtensions.cs
@@ -19,7 +19,7 @@ public static IServiceCollection AddModEndpointsFromAssembly(
services.TryAddSingleton();
//ServiceEndpoint components
- services.TryAddKeyedSingleton(
+ services.TryAddKeyedSingleton(
ServiceEndpointDefinitions.DefaultUriResolverName);
services.TryAddSingleton();
diff --git a/src/ModEndpoints/ModEndpoints.csproj b/src/ModEndpoints/ModEndpoints.csproj
index 6019101..6d0d89b 100644
--- a/src/ModEndpoints/ModEndpoints.csproj
+++ b/src/ModEndpoints/ModEndpoints.csproj
@@ -21,8 +21,8 @@
-
-
+
+
diff --git a/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs b/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs
index e8022bc..5437b59 100644
--- a/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs
+++ b/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs
@@ -87,7 +87,7 @@ public async ValueTask ToResponseAsync(
return result.ToResponse(SuccessfulResponseType.ResetContent);
default:
return result.ToResponse();
- };
+ }
}
public async ValueTask ToResponseAsync(