Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

<Version>0.3.2</Version>
<Version>0.3.3</Version>
</PropertyGroup>
</Project>
3 changes: 2 additions & 1 deletion src/ModEndpoints.RemoteServices/DefaultClientName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ModEndpoints.RemoteServices;
internal class DefaultClientName
{
private const string InvalidRequestType = "Request type should not be generic type parameter.";
public static string Resolve(IServiceRequestMarker request)
{
var requestType = request.GetType();
Expand All @@ -21,7 +22,7 @@ private static string ResolveInternal(Type requestType)
var requestName = requestType.AssemblyQualifiedName;
if (string.IsNullOrWhiteSpace(requestName))
{
throw new ArgumentException("Request type should not be generic.", nameof(requestType));
throw new ArgumentException(InvalidRequestType, nameof(requestType));
}
return $"{requestName}.Client";
}
Expand Down
24 changes: 15 additions & 9 deletions src/ModEndpoints.RemoteServices/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
namespace ModEndpoints.RemoteServices;
public static class DependencyInjectionExtensions
{
private const string ClientAlreadyExists = "A client with name {0} already exists.";
private const string ClientDoesNotExist = "A client with name {0} does not exist.";
private const string ChannelAlreadyRegistered = "A channel for request type {0} is already registered.";
private const string ChannelCannotBeRegistered = "Channel couldn't be registered for request type {0} and client name {1}.";

public static IServiceCollection AddRemoteServiceWithNewClient<TRequest>(
this IServiceCollection services,
string baseAddress,
Expand Down Expand Up @@ -65,7 +70,7 @@ public static IServiceCollection AddRemoteServiceWithNewClient<TRequest>(
{
if (ServiceChannelRegistry.Instance.DoesClientExist(clientName))
{
throw new InvalidOperationException($"A client with name {clientName} already exists.");
throw new InvalidOperationException(string.Format(ClientAlreadyExists, clientName));
}

return services.AddRemoteServiceWithNewClientInternal(
Expand All @@ -82,7 +87,7 @@ public static IServiceCollection AddRemoteServiceToExistingClient<TRequest>(
{
if (!ServiceChannelRegistry.Instance.DoesClientExist(clientName))
{
throw new InvalidOperationException($"A client with name {clientName} does not exist.");
throw new InvalidOperationException(string.Format(ClientDoesNotExist, clientName));
}
return services.AddRemoteServiceToExistingClientInternal(typeof(TRequest), clientName);
}
Expand All @@ -97,7 +102,7 @@ public static IServiceCollection AddRemoteServicesWithNewClient(
{
if (ServiceChannelRegistry.Instance.DoesClientExist(clientName))
{
throw new InvalidOperationException($"A client with name {clientName} already exists.");
throw new InvalidOperationException(string.Format(ClientAlreadyExists, clientName));
}
var requestTypes = fromAssembly
.DefinedTypes
Expand Down Expand Up @@ -131,7 +136,7 @@ public static IServiceCollection AddRemoteServicesToExistingClient(
{
if (!ServiceChannelRegistry.Instance.DoesClientExist(clientName))
{
throw new InvalidOperationException($"A client with name {clientName} does not exist.");
throw new InvalidOperationException(string.Format(ClientDoesNotExist, clientName));
}
var requestTypes = fromAssembly
.DefinedTypes
Expand Down Expand Up @@ -161,7 +166,7 @@ private static IServiceCollection AddRemoteServiceWithNewClientInternal(
{
if (ServiceChannelRegistry.Instance.IsRequestRegistered(requestType))
{
throw new InvalidOperationException($"A channel for request type {requestType} is already registered.");
throw new InvalidOperationException(string.Format(ChannelAlreadyRegistered, requestType));
}
services.AddRemoteServicesCore();
services.AddClientInternal(
Expand All @@ -170,7 +175,7 @@ private static IServiceCollection AddRemoteServiceWithNewClientInternal(
configureClientBuilder);
if (!ServiceChannelRegistry.Instance.RegisterRequest(requestType, clientName))
{
throw new InvalidOperationException($"Channel couldn't be registered for request type {requestType} and client name {clientName}.");
throw new InvalidOperationException(string.Format(ChannelCannotBeRegistered, requestType, clientName));
}
return services;
}
Expand All @@ -182,11 +187,11 @@ private static IServiceCollection AddRemoteServiceToExistingClientInternal(
{
if (ServiceChannelRegistry.Instance.IsRequestRegistered(requestType))
{
throw new InvalidOperationException($"A channel for request type {requestType} is already registered.");
throw new InvalidOperationException(string.Format(ChannelAlreadyRegistered, requestType));
}
if (!ServiceChannelRegistry.Instance.RegisterRequest(requestType, clientName))
{
throw new InvalidOperationException($"Channel couldn't be registered for request type {requestType} and client name {clientName}.");
throw new InvalidOperationException(string.Format(ChannelCannotBeRegistered, requestType, clientName));
}
return services;
}
Expand All @@ -207,7 +212,8 @@ private static IServiceCollection AddClientInternal(
private static IServiceCollection AddRemoteServicesCore(
this IServiceCollection services)
{
services.TryAddTransient<IServiceEndpointUriResolver, ServiceEndpointUriResolver>();
services.TryAddKeyedSingleton<IServiceEndpointUriResolver, ServiceEndpointUriResolver>(
ServiceEndpointDefinitions.DefaultUriResolverName);
services.TryAddTransient<IServiceChannel, ServiceChannel>();
return services;
}
Expand Down
6 changes: 4 additions & 2 deletions src/ModEndpoints.RemoteServices/IServiceChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
CancellationToken ct,
MediaTypeHeaderValue? mediaType = null,
JsonSerializerOptions? jsonSerializerOptions = null,
Action<HttpRequestHeaders>? configureRequestHeaders = null)
Action<HttpRequestHeaders>? configureRequestHeaders = null,
string? uriResolverName = null)
where TRequest : IServiceRequest<TResponse>
where TResponse : notnull;
Task<Result> SendAsync<TRequest>(
TRequest req,
CancellationToken ct,
MediaTypeHeaderValue? mediaType = null,
JsonSerializerOptions? jsonSerializerOptions = null,
Action<HttpRequestHeaders>? configureRequestHeaders = null)
Action<HttpRequestHeaders>? configureRequestHeaders = null,
string? uriResolverName = null)
where TRequest : IServiceRequest;
}
81 changes: 48 additions & 33 deletions src/ModEndpoints.RemoteServices/ServiceChannel.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using ModEndpoints.RemoteServices.Core;
using ModResults;

namespace ModEndpoints.RemoteServices;

public class ServiceChannel(
IHttpClientFactory clientFactory,
IServiceEndpointUriResolver uriResolver)
IServiceProvider serviceProvider)
: IServiceChannel
{
private const string NoChannelRegistrationFound = "No channel registration found for request type {0}.";

public async Task<Result<TResponse>> SendAsync<TRequest, TResponse>(
TRequest req,
CancellationToken ct,
MediaTypeHeaderValue? mediaType = null,
JsonSerializerOptions? jsonSerializerOptions = null,
Action<HttpRequestHeaders>? configureRequestHeaders = null)
Action<HttpRequestHeaders>? configureRequestHeaders = null,
string? uriResolverName = null)
where TRequest : IServiceRequest<TResponse>
where TResponse : notnull
{
try
{
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
{
return Result<TResponse>.Fail(requestUriResult);
}
if (!ServiceChannelRegistry.Instance.IsRequestRegistered<TRequest>(out var clientName))
{
return Result<TResponse>.CriticalError($"No channel registration found for request type {typeof(TRequest)}");
}
using (HttpRequestMessage httpReq = new(HttpMethod.Post, requestUriResult.Value))
using (var scope = serviceProvider.CreateScope())
{
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
configureRequestHeaders?.Invoke(httpReq.Headers);
var client = clientFactory.CreateClient(clientName);
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService<IServiceEndpointUriResolver>(
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
{
return Result<TResponse>.Fail(requestUriResult);
}
if (!ServiceChannelRegistry.Instance.IsRequestRegistered<TRequest>(out var clientName))
{
return Result<TResponse>.CriticalError(string.Format(NoChannelRegistrationFound, typeof(TRequest)));
}
using (HttpRequestMessage httpReq = new(HttpMethod.Post, requestUriResult.Value))
{
return await httpResponse.DeserializeResultAsync<TResponse>(ct);
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
configureRequestHeaders?.Invoke(httpReq.Headers);
var client = clientFactory.CreateClient(clientName);
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
{
return await httpResponse.DeserializeResultAsync<TResponse>(ct);
}
}
}
}
Expand All @@ -53,28 +62,34 @@ public async Task<Result> SendAsync<TRequest>(
CancellationToken ct,
MediaTypeHeaderValue? mediaType = null,
JsonSerializerOptions? jsonSerializerOptions = null,
Action<HttpRequestHeaders>? configureRequestHeaders = null)
Action<HttpRequestHeaders>? configureRequestHeaders = null,
string? uriResolverName = null)
where TRequest : IServiceRequest
{
try
{
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
using (var scope = serviceProvider.CreateScope())
{
return Result.Fail(requestUriResult);
}
if (!ServiceChannelRegistry.Instance.IsRequestRegistered<TRequest>(out var clientName))
{
return Result.CriticalError($"No channel registration found for request type {typeof(TRequest)}");
}
using (HttpRequestMessage httpReq = new(HttpMethod.Post, requestUriResult.Value))
{
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
configureRequestHeaders?.Invoke(httpReq.Headers);
var client = clientFactory.CreateClient(clientName);
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
var uriResolver = scope.ServiceProvider.GetRequiredKeyedService<IServiceEndpointUriResolver>(
uriResolverName ?? ServiceEndpointDefinitions.DefaultUriResolverName);
var requestUriResult = uriResolver.Resolve(req);
if (requestUriResult.IsFailed)
{
return Result.Fail(requestUriResult);
}
if (!ServiceChannelRegistry.Instance.IsRequestRegistered<TRequest>(out var clientName))
{
return Result.CriticalError(string.Format(NoChannelRegistrationFound, typeof(TRequest)));
}
using (HttpRequestMessage httpReq = new(HttpMethod.Post, requestUriResult.Value))
{
return await httpResponse.DeserializeResultAsync(ct);
httpReq.Content = JsonContent.Create(req, mediaType, jsonSerializerOptions);
configureRequestHeaders?.Invoke(httpReq.Headers);
var client = clientFactory.CreateClient(clientName);
using (var httpResponse = await client.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct))
{
return await httpResponse.DeserializeResultAsync(ct);
}
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/ModEndpoints.RemoteServices/ServiceEndpointDefinitions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace ModEndpoints.RemoteServices;
public static class ServiceEndpointDefinitions
{
public const string DefaultUriResolverName = "DefaultUriResolver";
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace ModEndpoints.RemoteServices;
public class ServiceEndpointUriResolver : IServiceEndpointUriResolver
{
private const string CannotResolveServiceEndpointUri = "Cannot resolve request uri for service endpoint.";
public Result<string> Resolve(IServiceRequestMarker req)
{
var requestType = req.GetType();
Expand All @@ -21,7 +22,7 @@ private Result<string> ResolveInternal(Type requestType)
var requestName = requestType.FullName;
if (string.IsNullOrWhiteSpace(requestName))
{
return Result<string>.CriticalError("Cannot resolve request uri for service endpoint.");
return Result<string>.CriticalError(CannotResolveServiceEndpointUri);
}
return $"{requestName}.Endpoint";
}
Expand Down
8 changes: 7 additions & 1 deletion src/ModEndpoints/DependencyInjectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ public static IServiceCollection AddModEndpointsFromAssembly(
this IServiceCollection services,
Assembly assembly)
{
//WebResultEndpoint components
services.TryAddKeyedSingleton<IResultToResponseMapper, DefaultResultToResponseMapper>(
WebResultEndpointDefinitions.DefaultResultToResponseMapperName);
services.TryAddScoped<ILocationStore, DefaultLocationStore>();
services.TryAddSingleton<IResultToResponseMapProvider, DefaultResultToResponseMapProvider>();
services.TryAddTransient<IServiceEndpointUriResolver, ServiceEndpointUriResolver>();

//ServiceEndpoint components
services.TryAddKeyedSingleton<IServiceEndpointUriResolver, ServiceEndpointUriResolver>(
ServiceEndpointDefinitions.DefaultUriResolverName);
services.TryAddSingleton<IUriResolverProvider, DefaultUriResolverProvider>();

services.AddHttpContextAccessor();
return services.AddModEndpointsFromAssemblyCore(assembly);
}
Expand Down
26 changes: 26 additions & 0 deletions src/ModEndpoints/[ServiceEndpoint]/DefaultUriResolverProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Concurrent;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using ModEndpoints.Core;
using ModEndpoints.RemoteServices;

namespace ModEndpoints;

public class DefaultUriResolverProvider :
IUriResolverProvider
{
public IServiceEndpointUriResolver GetResolver(
IServiceProvider serviceProvider,
ServiceEndpointConfigurator endpoint)
{
var resolverName = GetResolverName(endpoint);
var resolver = serviceProvider.GetRequiredKeyedService<IServiceEndpointUriResolver>(resolverName);
return resolver;
}

private string GetResolverName(ServiceEndpointConfigurator endpoint)
{
return endpoint.GetType().GetCustomAttribute<UriResolverAttribute>()?.Name ??
ServiceEndpointDefinitions.DefaultUriResolverName;
}
}
19 changes: 19 additions & 0 deletions src/ModEndpoints/[ServiceEndpoint]/IUriResolverProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using ModEndpoints.Core;
using ModEndpoints.RemoteServices;

namespace ModEndpoints;

/// <summary>
/// Helper methods for service endpoint uri resolvers.
/// </summary>
public interface IUriResolverProvider
{
/// <summary>
/// Gets the uri resolver for a particular <see cref="ServiceEndpointConfigurator"/> implementation,
/// that will be used to determine the uri, endpoint will be mapped to.
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="endpoint"></param>
/// <returns></returns>
IServiceEndpointUriResolver GetResolver(IServiceProvider serviceProvider, ServiceEndpointConfigurator endpoint);
}
6 changes: 4 additions & 2 deletions src/ModEndpoints/[ServiceEndpoint]/ServiceEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ protected override ValueTask<Result<TResultValue>> HandleInvalidValidationResult
IEndpointRouteBuilder builder,
IRouteGroupConfigurator? parentRouteGroup)
{
var uriResolver = serviceProvider.GetRequiredService<IServiceEndpointUriResolver>();
var uriResolverProvider = serviceProvider.GetRequiredService<IUriResolverProvider>();
var uriResolver = uriResolverProvider.GetResolver(serviceProvider, this);
var patternResult = uriResolver.Resolve<TRequest>();
if (patternResult.IsOk)
{
Expand Down Expand Up @@ -57,7 +58,8 @@ protected override ValueTask<Result> HandleInvalidValidationResultAsync(
IEndpointRouteBuilder builder,
IRouteGroupConfigurator? parentRouteGroup)
{
var uriResolver = serviceProvider.GetRequiredService<IServiceEndpointUriResolver>();
var uriResolverProvider = serviceProvider.GetRequiredService<IUriResolverProvider>();
var uriResolver = uriResolverProvider.GetResolver(serviceProvider, this);
var patternResult = uriResolver.Resolve<TRequest>();
if (patternResult.IsOk)
{
Expand Down
16 changes: 16 additions & 0 deletions src/ModEndpoints/[ServiceEndpoint]/UriResolverAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace ModEndpoints;

using ModEndpoints.Core;

/// <summary>
/// Sets the uri resolver name for that <see cref="IServiceEndpoint"/> implementation.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class UriResolverAttribute : Attribute
{
public string Name { get; init; }
public UriResolverAttribute(string Name)
{
this.Name = Name;
}
}