-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
using System.Security.Claims; | ||
|
||
using MediatR.CommandQuery.Dispatcher; | ||
|
||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Routing; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace MediatR.CommandQuery.Endpoints; | ||
|
||
public class DispatcherEndpoint : IFeatureEndpoint | ||
{ | ||
private readonly ISender _sender; | ||
private readonly DispatcherOptions _dispatcherOptions; | ||
|
||
public DispatcherEndpoint(ISender sender, IOptions<DispatcherOptions> dispatcherOptions) | ||
{ | ||
_sender = sender; | ||
_dispatcherOptions = dispatcherOptions.Value; | ||
} | ||
|
||
public void AddRoutes(IEndpointRouteBuilder app) | ||
{ | ||
var group = app | ||
.MapGroup(_dispatcherOptions.RoutePrefix); | ||
|
||
group | ||
.MapPost(_dispatcherOptions.SendRoute, Send) | ||
.ExcludeFromDescription(); | ||
} | ||
|
||
protected virtual async Task<IResult> Send( | ||
[FromBody] DispatchRequest dispatchRequest, | ||
ClaimsPrincipal? user = default, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
try | ||
{ | ||
var request = dispatchRequest.Request; | ||
var result = await _sender.Send(request, cancellationToken); | ||
return Results.Ok(result); | ||
} | ||
catch (Exception ex) | ||
{ | ||
var details = ex.ToProblemDetails(); | ||
return Results.Problem(details); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
using MediatR.CommandQuery.Converters; | ||
|
||
namespace MediatR.CommandQuery.Dispatcher; | ||
|
||
public class DispatchRequest | ||
{ | ||
[JsonConverter(typeof(PolymorphicConverter<IBaseRequest>))] | ||
public IBaseRequest Request { get; set; } = null!; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace MediatR.CommandQuery.Dispatcher; | ||
|
||
public class DispatcherOptions | ||
{ | ||
public string RoutePrefix { get; set; } = "/api"; | ||
|
||
public string SendRoute { get; set; } = "/dispatcher"; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace MediatR.CommandQuery.Dispatcher; | ||
|
||
public interface IDispatcher | ||
{ | ||
Task<TResponse?> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace MediatR.CommandQuery.Dispatcher; | ||
|
||
public class MediatorDispatcher : IDispatcher | ||
{ | ||
private readonly ISender _sender; | ||
|
||
public MediatorDispatcher(ISender sender) | ||
{ | ||
_sender = sender; | ||
} | ||
|
||
public async Task<TResponse?> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default) | ||
{ | ||
return await _sender.Send(request, cancellationToken); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
using System.Net.Http.Json; | ||
using System.Text.Json; | ||
|
||
using MediatR.CommandQuery.Models; | ||
|
||
using Microsoft.Extensions.Options; | ||
|
||
namespace MediatR.CommandQuery.Dispatcher; | ||
|
||
public class RemoteDispatcher : IDispatcher | ||
{ | ||
private readonly HttpClient _httpClient; | ||
private readonly JsonSerializerOptions _serializerOptions; | ||
private readonly DispatcherOptions _dispatcherOptions; | ||
|
||
public RemoteDispatcher(HttpClient httpClient, JsonSerializerOptions serializerOptions, IOptions<DispatcherOptions> dispatcherOptions) | ||
{ | ||
_httpClient = httpClient; | ||
_serializerOptions = serializerOptions; | ||
_dispatcherOptions = dispatcherOptions.Value; | ||
} | ||
|
||
public async Task<TResponse?> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default) | ||
{ | ||
var requestUri = Combine(_dispatcherOptions.RoutePrefix, _dispatcherOptions.SendRoute); | ||
|
||
var dispatchRequest = new DispatchRequest { Request = request }; | ||
|
||
var responseMessage = await _httpClient.PostAsJsonAsync( | ||
requestUri: requestUri, | ||
value: dispatchRequest, | ||
options: _serializerOptions, | ||
cancellationToken: cancellationToken); | ||
|
||
await EnsureSuccessStatusCode(responseMessage, cancellationToken); | ||
|
||
return await responseMessage.Content.ReadFromJsonAsync<TResponse>( | ||
options: _serializerOptions, | ||
cancellationToken: cancellationToken); | ||
} | ||
|
||
private async Task EnsureSuccessStatusCode(HttpResponseMessage responseMessage, CancellationToken cancellationToken = default) | ||
{ | ||
if (responseMessage.IsSuccessStatusCode) | ||
return; | ||
|
||
var message = $"Response status code does not indicate success: {responseMessage.StatusCode} ({responseMessage.ReasonPhrase})."; | ||
|
||
var mediaType = responseMessage.Content.Headers.ContentType?.MediaType; | ||
if (!string.Equals(mediaType, "application/problem+json", StringComparison.OrdinalIgnoreCase)) | ||
throw new HttpRequestException(message, inner: null, responseMessage.StatusCode); | ||
|
||
var problemDetails = await responseMessage.Content.ReadFromJsonAsync<ProblemDetails>( | ||
options: _serializerOptions, | ||
cancellationToken: cancellationToken); | ||
|
||
if (problemDetails == null) | ||
throw new HttpRequestException(message, inner: null, responseMessage.StatusCode); | ||
|
||
var status = (System.Net.HttpStatusCode?)problemDetails.Status; | ||
status ??= responseMessage.StatusCode; | ||
|
||
var problemMessage = problemDetails.Title | ||
?? responseMessage.ReasonPhrase | ||
?? "Internal Server Error"; | ||
|
||
if (!string.IsNullOrEmpty(problemDetails.Detail)) | ||
problemMessage = $"{problemMessage} {problemDetails.Detail}"; | ||
|
||
throw new HttpRequestException( | ||
message: problemMessage, | ||
inner: null, | ||
statusCode: status); | ||
} | ||
|
||
private static string Combine(string first, string second) | ||
{ | ||
if (string.IsNullOrEmpty(first)) | ||
return second; | ||
|
||
if (string.IsNullOrEmpty(second)) | ||
return first; | ||
|
||
bool hasSeparator = first[^1] == '/' || second[0] == '/'; | ||
|
||
return hasSeparator | ||
? string.Concat(first, second) | ||
: $"{first}/{second}"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
using MediatR.CommandQuery.Dispatcher; | ||
using MediatR.CommandQuery.Models; | ||
|
||
namespace MediatR.CommandQuery; | ||
|
||
[JsonSourceGenerationOptions( | ||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, | ||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase | ||
)] | ||
[JsonSerializable(typeof(DispatchRequest))] | ||
[JsonSerializable(typeof(ProblemDetails))] | ||
public partial class MediatorJsonContext : JsonSerializerContext; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
using System.Text.Json.Serialization; | ||
|
||
namespace MediatR.CommandQuery.Models; | ||
|
||
/// <summary> | ||
/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. | ||
/// </summary> | ||
public class ProblemDetails | ||
{ | ||
/// <summary> | ||
/// The content-type for a problem json response | ||
/// </summary> | ||
public const string ContentType = "application/problem+json"; | ||
|
||
/// <summary> | ||
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when | ||
/// dereferenced, it provide human-readable documentation for the problem type | ||
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be | ||
/// "about:blank". | ||
/// </summary> | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
[JsonPropertyOrder(-5)] | ||
[JsonPropertyName("type")] | ||
public string? Type { get; set; } | ||
|
||
/// <summary> | ||
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence | ||
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation; | ||
/// see[RFC7231], Section 3.4). | ||
/// </summary> | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
[JsonPropertyOrder(-4)] | ||
[JsonPropertyName("title")] | ||
public string? Title { get; set; } | ||
|
||
/// <summary> | ||
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. | ||
/// </summary> | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
[JsonPropertyOrder(-3)] | ||
[JsonPropertyName("status")] | ||
public int? Status { get; set; } | ||
|
||
/// <summary> | ||
/// A human-readable explanation specific to this occurrence of the problem. | ||
/// </summary> | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
[JsonPropertyOrder(-2)] | ||
[JsonPropertyName("detail")] | ||
public string? Detail { get; set; } | ||
|
||
/// <summary> | ||
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. | ||
/// </summary> | ||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | ||
[JsonPropertyOrder(-1)] | ||
[JsonPropertyName("instance")] | ||
public string? Instance { get; set; } | ||
|
||
/// <summary> | ||
/// Gets the validation errors associated with this instance of problem details | ||
/// </summary> | ||
[JsonPropertyName("errors")] | ||
public IDictionary<string, string[]> Errors { get; set; } = new Dictionary<string, string[]>(StringComparer.Ordinal); | ||
|
||
/// <summary> | ||
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members. | ||
/// <para> | ||
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as | ||
/// other members of a problem type. | ||
/// </para> | ||
/// </summary> | ||
/// <remarks> | ||
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters. | ||
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. | ||
/// </remarks> | ||
[JsonExtensionData] | ||
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal); | ||
} |