Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.10.0 #88

Merged
merged 2 commits into from
Jan 26, 2024
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## v3.10.0
- *Enhancement*: The `WebApiPublisher` publishing methods have been simplified (breaking change), primarily through the use of a new _argument_ that encapsulates the various related options. This will enable the addition of further options in the future without resulting in breaking changes or adding unneccessary complexities. The related [`README`](./src/CoreEx.AspNetCore/WebApis/README.md) has been updated to document.
- *Enhancement*: Added `ValidationUseJsonNames` to `SettingsBase` (defaults to `true`) to allow setting `ValidationArgs.DefaultUseJsonNames` to be configurable.

## v3.9.0
- *Enhancement*: A new `Abstractions.ServiceBusMessageActions` has been created to encapsulate either a `Microsoft.Azure.WebJobs.ServiceBus.ServiceBusMessageActions` (existing [_in-process_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) function support) or `Microsoft.Azure.Functions.Worker.ServiceBusMessageActions` (new [_isolated_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) function support) and used internally. Implicit conversion is enabled to simplify usage; existing projects will need to be recompiled. The latter capability does not support `RenewAsync` and as such this capability is no longer leveraged for consistency; review documented [`PeekLock`](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cextensionv5&pivots=programming-language-csharp#peeklock-behavior) behavior to get desired outcome.
- *Enhancement*: The `Result`, `Result<T>`, `PagingArgs` and `PagingResult` have had `IEquatable` added to enable equality comparisons.
Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.9.0</Version>
<Version>3.10.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, HrS
[OpenApiRequestBody(MediaTypeNames.Application.Json, typeof(EmployeeVerificationRequest), Description = "The **EmployeeVerification** payload")]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Accepted, contentType: MediaTypeNames.Text.Plain, bodyType: typeof(string), Description = "The OK response")]
public Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request)
=> _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap());
=> _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs<EmployeeVerificationRequest>(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() });
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class AspNetCoreServiceCollectionExtensions
/// <summary>
/// Checks that the <see cref="IServiceCollection"/> is not null.
/// </summary>
private static IServiceCollection CheckServices(IServiceCollection services) => services ?? throw new ArgumentNullException(nameof(services));
private static IServiceCollection CheckServices(IServiceCollection services) => services.ThrowIfNull(nameof(services));

/// <summary>
/// Adds the <see cref="WebApi"/> as a scoped service.
Expand Down
18 changes: 4 additions & 14 deletions src/CoreEx.AspNetCore/HealthChecks/HealthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,11 @@ namespace CoreEx.AspNetCore.HealthChecks
/// <summary>
/// Provides the Health Check service.
/// </summary>
public class HealthService
public class HealthService(SettingsBase settings, HealthCheckService healthCheckService, IJsonSerializer jsonSerializer)
{
private readonly SettingsBase _settings;
private readonly HealthCheckService _healthCheckService;
private readonly IJsonSerializer _jsonSerializer;

/// <summary>
/// Initializes a new instance of the <see cref="HealthService"/> class.
/// </summary>
public HealthService(SettingsBase settings, HealthCheckService healthCheckService, IJsonSerializer jsonSerializer)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
_healthCheckService = healthCheckService ?? throw new ArgumentNullException(nameof(healthCheckService));
_jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
}
private readonly SettingsBase _settings = settings.ThrowIfNull(nameof(settings));
private readonly HealthCheckService _healthCheckService = healthCheckService.ThrowIfNull(nameof(healthCheckService));
private readonly IJsonSerializer _jsonSerializer = jsonSerializer.ThrowIfNull(nameof(jsonSerializer));

/// <summary>
/// Runs the health check and returns JSON result.
Expand Down
10 changes: 4 additions & 6 deletions src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public static class HttpResultExtensions
/// <remarks>This will automatically invoke <see cref="ApplyETag(HttpRequest, string)"/> where there is an <see cref="HttpRequestOptions.ETag"/> value.</remarks>
public static HttpRequest ApplyRequestOptions(this HttpRequest httpRequest, HttpRequestOptions? requestOptions)
{
if (httpRequest == null)
throw new ArgumentNullException(nameof(httpRequest));
httpRequest.ThrowIfNull(nameof(httpRequest));

if (requestOptions == null)
return httpRequest;
Expand Down Expand Up @@ -87,8 +86,7 @@ public static HttpRequest ApplyETag(this HttpRequest httpRequest, string? etag)
/// <returns>The <see cref="HttpRequestJsonValue{T}"/>.</returns>
public static async Task<HttpRequestJsonValue<T>> ReadAsJsonValueAsync<T>(this HttpRequest httpRequest, IJsonSerializer jsonSerializer, bool valueIsRequired = true, IValidator<T>? validator = null, CancellationToken cancellationToken = default)
{
if (httpRequest == null)
throw new ArgumentNullException(nameof(httpRequest));
httpRequest.ThrowIfNull(nameof(httpRequest));

var content = await BinaryData.FromStreamAsync(httpRequest.Body, cancellationToken).ConfigureAwait(false);
var jv = new HttpRequestJsonValue<T>();
Expand All @@ -97,7 +95,7 @@ public static async Task<HttpRequestJsonValue<T>> ReadAsJsonValueAsync<T>(this H
try
{
if (content.ToMemory().Length > 0)
jv.Value = (jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer))).Deserialize<T>(content)!;
jv.Value = jsonSerializer.ThrowIfNull(nameof(jsonSerializer)).Deserialize<T>(content)!;

if (valueIsRequired && jv.Value == null)
jv.ValidationException = new ValidationException($"{InvalidJsonMessagePrefix} Value is mandatory.");
Expand Down Expand Up @@ -138,7 +136,7 @@ public static async Task<HttpRequestJsonValue<T>> ReadAsJsonValueAsync<T>(this H
/// </summary>
/// <param name="httpRequest">The <see cref="HttpRequest"/>.</param>
/// <returns>The <see cref="WebApiRequestOptions"/>.</returns>
public static WebApiRequestOptions GetRequestOptions(this HttpRequest httpRequest) => new(httpRequest ?? throw new ArgumentNullException(nameof(httpRequest)));
public static WebApiRequestOptions GetRequestOptions(this HttpRequest httpRequest) => new(httpRequest.ThrowIfNull(nameof(httpRequest)));

/// <summary>
/// Adds the <see cref="PagingArgs"/> to the <see cref="IHeaderDictionary"/>.
Expand Down
47 changes: 40 additions & 7 deletions src/CoreEx.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public class EmployeeFunction

## WebApiPublish

The [`WebApiPublisher`](./WebApis/WebApiPublisher.cs) class should be leveraged for fire-and-forget style APIs, where the message is received, validated and then published as an event for out-of-process decoupled processing.
The [`WebApiPublisher`](./WebApis/WebApiPublisher.cs) class should be leveraged for _fire-and-forget_ style APIs, where the message is received, validated and then published as an event for out-of-process decoupled processing.

The `WebApiPublish` extends (inherits) [`WebApiBase`](./WebApis/WebApiBase.cs) that provides the base `RunAsync` method described [above](#WebApi).

Expand All @@ -181,12 +181,46 @@ The `WebApiPublisher` constructor takes an [`IEventPublisher`](../CoreEx/Events/

### Supported HTTP methods

A publish should be performed using an HTTP `POST` and as such this is the only HTTP method supported. The `WebApiPublish` provides the following overloads depending on need. Where a generic `Type` is specified, either `TValue` being the request content body and/or `TResult` being the response body, this signifies that `WebApi` will manage the underlying JSON serialization:
A publish should be performed using an HTTP `POST` and as such this is the only HTTP method supported. The `WebApiPublish` provides the following overloads depending on need.

HTTP | Method | Description
-|-|-
`POST` | `PublishAsync<TValue>()` | Publish a single message/event with `TValue` being the request content body.
`POST` | `PublishAsync<TColl, TItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem`.
`POST` | `PublishValueAsync<TValue>()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized).
`POST` | `PublishAsync<TValue, TEventValue>()` | Publish a single message/event with `TValue` being the request content body mapping to the specified event value type.
`POST` | `PublishValueAsync<TValue, TEventValue>()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized) mapping to the specified event value type.
- | -
`POST` | `PublishCollectionAsync<TColl, TItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem` being the request content body.
`POST` | `PublishCollectionValueAsync<TColl, TItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem` being the specified value (preivously deserialized).
`POST` | `PublishCollectionAsync<TColl, TItem, TEventItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem` being the request content body mapping to the specified event value type.
`POST` | `PublishCollectionValueAsync<TColl, TItem, TEventItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem` being the specified value (preivously deserialized) mapping to the specified event value type.

<br/>

### Argument

Depending on the overload used (as defined above), an optional _argument_ can be specified that provides additional opportunities to configure and add additional logic into the underlying publishing orchestration.

The following argurment types are supported:
- [`WebApiPublisherArgs<TValue>`](./WebApis/WebApiPublisherArgsT.cs) - single message with no mapping.
- [`WebApiPublisherArgs<TValue, TEventValue>`](./WebApis/WebApiPublisherArgsT2.cs) - single message _with_ mapping.
- [`WebApiPublisherCollectionArgs<TColl, TItem>`](./WebApis/WebApiPublisherCollectionArgsT.cs) - collection of messages with no mapping.
- [`WebApiPublisherCollectionArgs<TColl, TItem, TEventItem>`](./WebApis/WebApiPublisherCollectionArgsT2.cs) - collection of messages _with_ mapping.

The arguments will have the following properties depending on the supported functionality. The sequence defines the order in which each of the properties is enacted (orchestrated) internally. Where a failure or exception occurs then the execution will be aborted and the corresponding `IActionResult` returned (including the likes of logging etc. where applicable).

Property | Description | Sequence
-|-
`EventName` | The event destintion name (e.g. Queue or Topic name) where applicable. | N/A
`StatusCode` | The resulting status code where successful. Defaults to `204-Accepted`. | N/A
`OperationType` | The [`OperationType`](../CoreEx/OperationType.cs). Defaults to `OperationType.Unspecified`. | N/A
`MaxCollectionSize` | The maximum collection size allowed/supported. | 1
`OnBeforeValidateAsync` | The function to be invoked before the request value is validated; opportunity to modify contents. | 2
`Validator` | The `IValidator<T>` to validate the request value. | 3
`OnBeforeEventAsync` | The function to be invoked after validation / before event; opportunity to modify contents. | 4
`Mapper` | The `IMapper<TSource, TDestination>` override. | 5
`OnEvent` | The action to be invoked once converted to an [`EventData`](../CoreEx/Events/EventData.cs); opportunity to modify contents. | 6
`CreateSuccessResult` | The function to be invoked to create/override the success `IActionResult`. | 7

<br/>

Expand All @@ -198,7 +232,7 @@ A request body is mandatory and must be serialized JSON as per the specified gen

### Response

The response HTTP status code is `204-Accepted` (default) with no content.
The response HTTP status code is `204-Accepted` (default) with no content. This can be overridden using the arguments `StatusCode` property.

<br/>

Expand All @@ -218,7 +252,6 @@ public class HttpTriggerQueueVerificationFunction
_settings = settings;
}

[FunctionName(nameof(HttpTriggerQueueVerificationFunction))]
public Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request)
=> _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap());
```
=> _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs<EmployeeVerificationRequest>(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() });
}
21 changes: 6 additions & 15 deletions src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,20 @@ namespace CoreEx.AspNetCore.WebApis
/// An attribute that specifies the expected request <b>body</b> <see cref="Type"/> that the action/operation accepts and the supported request content types.
/// </summary>
/// <remarks>The is used to enable <i>Swagger/Swashbuckle</i> generated documentation where the operation does not explicitly define the body as a method parameter; i.e. via <see cref="Microsoft.AspNetCore.Mvc.FromBodyAttribute"/>.</remarks>
/// <param name="type">The <b>body</b> <see cref="Type"/>.</param>
/// <param name="contentTypes">The <b>body</b> content type(s). Defaults to <see cref="MediaTypeNames.Application.Json"/>.</param>
/// <exception cref="ArgumentNullException"></exception>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class AcceptsBodyAttribute : Attribute
public sealed class AcceptsBodyAttribute(Type type, params string[] contentTypes) : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AcceptsBodyAttribute"/> class.
/// </summary>
/// <param name="type">The <b>body</b> <see cref="Type"/>.</param>
/// <param name="contentTypes">The <b>body</b> content type(s). Defaults to <see cref="MediaTypeNames.Application.Json"/>.</param>
/// <exception cref="ArgumentNullException"></exception>
public AcceptsBodyAttribute(Type type, params string[] contentTypes)
{
BodyType = type ?? throw new ArgumentNullException(nameof(type));
ContentTypes = contentTypes.Length == 0 ? new string[] { MediaTypeNames.Application.Json } : contentTypes;
}

/// <summary>
/// Gets the <b>body</b> <see cref="Type"/>.
/// </summary>
public Type BodyType { get; }
public Type BodyType { get; } = type.ThrowIfNull(nameof(type));

/// <summary>
/// Gets the <b>body</b> content type(s).
/// </summary>
public string[] ContentTypes { get; }
public string[] ContentTypes { get; } = contentTypes.Length == 0 ? [MediaTypeNames.Application.Json] : contentTypes;
}
}
9 changes: 2 additions & 7 deletions src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@ namespace CoreEx.AspNetCore.WebApis
/// <summary>
/// Represents an extended <see cref="StatusCodeResult"/> that enables customization of the <see cref="HttpResponse"/>.
/// </summary>
public class ExtendedStatusCodeResult : StatusCodeResult
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
public class ExtendedStatusCodeResult(HttpStatusCode statusCode) : StatusCodeResult((int)statusCode)
{
/// <summary>
/// Initializes a new instance of the <see cref="ExtendedStatusCodeResult"/> class.
/// </summary>
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
public ExtendedStatusCodeResult(HttpStatusCode statusCode) : base((int)statusCode) { }

/// <summary>
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders.Location"/> <see cref="Uri"/>.
/// </summary>
Expand Down
Loading
Loading