Skip to content

Commit 40b4fa8

Browse files
authored
- Enhance/simplify WebApiPublisher. (#88)
- Code optimizations in AspNetCore. - Validation UseJsonName setting. - Documentation tweaks. Signed-off-by: Eric Sibly [chullybun] <[email protected]>
1 parent 2981d4c commit 40b4fa8

34 files changed

+738
-1075
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Represents the **NuGet** versions.
44

5+
## v3.10.0
6+
- *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.
7+
- *Enhancement*: Added `ValidationUseJsonNames` to `SettingsBase` (defaults to `true`) to allow setting `ValidationArgs.DefaultUseJsonNames` to be configurable.
8+
59
## v3.9.0
610
- *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.
711
- *Enhancement*: The `Result`, `Result<T>`, `PagingArgs` and `PagingResult` have had `IEquatable` added to enable equality comparisons.

Common.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>3.9.0</Version>
3+
<Version>3.10.0</Version>
44
<LangVersion>preview</LangVersion>
55
<Authors>Avanade</Authors>
66
<Company>Avanade</Company>

samples/My.Hr/My.Hr.Functions/Functions/HttpTriggerQueueVerificationFunction.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ public HttpTriggerQueueVerificationFunction(WebApiPublisher webApiPublisher, HrS
3333
[OpenApiRequestBody(MediaTypeNames.Application.Json, typeof(EmployeeVerificationRequest), Description = "The **EmployeeVerification** payload")]
3434
[OpenApiResponseWithBody(statusCode: HttpStatusCode.Accepted, contentType: MediaTypeNames.Text.Plain, bodyType: typeof(string), Description = "The OK response")]
3535
public Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request)
36-
=> _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap());
36+
=> _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs<EmployeeVerificationRequest>(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() });
3737
}

src/CoreEx.AspNetCore/Abstractions/AspNetCoreServiceCollectionExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static class AspNetCoreServiceCollectionExtensions
1919
/// <summary>
2020
/// Checks that the <see cref="IServiceCollection"/> is not null.
2121
/// </summary>
22-
private static IServiceCollection CheckServices(IServiceCollection services) => services ?? throw new ArgumentNullException(nameof(services));
22+
private static IServiceCollection CheckServices(IServiceCollection services) => services.ThrowIfNull(nameof(services));
2323

2424
/// <summary>
2525
/// Adds the <see cref="WebApi"/> as a scoped service.

src/CoreEx.AspNetCore/HealthChecks/HealthService.cs

+4-14
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,11 @@ namespace CoreEx.AspNetCore.HealthChecks
1515
/// <summary>
1616
/// Provides the Health Check service.
1717
/// </summary>
18-
public class HealthService
18+
public class HealthService(SettingsBase settings, HealthCheckService healthCheckService, IJsonSerializer jsonSerializer)
1919
{
20-
private readonly SettingsBase _settings;
21-
private readonly HealthCheckService _healthCheckService;
22-
private readonly IJsonSerializer _jsonSerializer;
23-
24-
/// <summary>
25-
/// Initializes a new instance of the <see cref="HealthService"/> class.
26-
/// </summary>
27-
public HealthService(SettingsBase settings, HealthCheckService healthCheckService, IJsonSerializer jsonSerializer)
28-
{
29-
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
30-
_healthCheckService = healthCheckService ?? throw new ArgumentNullException(nameof(healthCheckService));
31-
_jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
32-
}
20+
private readonly SettingsBase _settings = settings.ThrowIfNull(nameof(settings));
21+
private readonly HealthCheckService _healthCheckService = healthCheckService.ThrowIfNull(nameof(healthCheckService));
22+
private readonly IJsonSerializer _jsonSerializer = jsonSerializer.ThrowIfNull(nameof(jsonSerializer));
3323

3424
/// <summary>
3525
/// Runs the health check and returns JSON result.

src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs

+4-6
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ public static class HttpResultExtensions
3838
/// <remarks>This will automatically invoke <see cref="ApplyETag(HttpRequest, string)"/> where there is an <see cref="HttpRequestOptions.ETag"/> value.</remarks>
3939
public static HttpRequest ApplyRequestOptions(this HttpRequest httpRequest, HttpRequestOptions? requestOptions)
4040
{
41-
if (httpRequest == null)
42-
throw new ArgumentNullException(nameof(httpRequest));
41+
httpRequest.ThrowIfNull(nameof(httpRequest));
4342

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

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

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

143141
/// <summary>
144142
/// Adds the <see cref="PagingArgs"/> to the <see cref="IHeaderDictionary"/>.

src/CoreEx.AspNetCore/README.md

+40-7
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public class EmployeeFunction
171171

172172
## WebApiPublish
173173

174-
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.
174+
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.
175175

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

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

182182
### Supported HTTP methods
183183

184-
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:
184+
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.
185185

186186
HTTP | Method | Description
187187
-|-|-
188188
`POST` | `PublishAsync<TValue>()` | Publish a single message/event with `TValue` being the request content body.
189-
`POST` | `PublishAsync<TColl, TItem>()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem`.
189+
`POST` | `PublishValueAsync<TValue>()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized).
190+
`POST` | `PublishAsync<TValue, TEventValue>()` | Publish a single message/event with `TValue` being the request content body mapping to the specified event value type.
191+
`POST` | `PublishValueAsync<TValue, TEventValue>()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized) mapping to the specified event value type.
192+
- | -
193+
`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.
194+
`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).
195+
`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.
196+
`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.
197+
198+
<br/>
199+
200+
### Argument
201+
202+
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.
203+
204+
The following argurment types are supported:
205+
- [`WebApiPublisherArgs<TValue>`](./WebApis/WebApiPublisherArgsT.cs) - single message with no mapping.
206+
- [`WebApiPublisherArgs<TValue, TEventValue>`](./WebApis/WebApiPublisherArgsT2.cs) - single message _with_ mapping.
207+
- [`WebApiPublisherCollectionArgs<TColl, TItem>`](./WebApis/WebApiPublisherCollectionArgsT.cs) - collection of messages with no mapping.
208+
- [`WebApiPublisherCollectionArgs<TColl, TItem, TEventItem>`](./WebApis/WebApiPublisherCollectionArgsT2.cs) - collection of messages _with_ mapping.
209+
210+
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).
211+
212+
Property | Description | Sequence
213+
-|-
214+
`EventName` | The event destintion name (e.g. Queue or Topic name) where applicable. | N/A
215+
`StatusCode` | The resulting status code where successful. Defaults to `204-Accepted`. | N/A
216+
`OperationType` | The [`OperationType`](../CoreEx/OperationType.cs). Defaults to `OperationType.Unspecified`. | N/A
217+
`MaxCollectionSize` | The maximum collection size allowed/supported. | 1
218+
`OnBeforeValidateAsync` | The function to be invoked before the request value is validated; opportunity to modify contents. | 2
219+
`Validator` | The `IValidator<T>` to validate the request value. | 3
220+
`OnBeforeEventAsync` | The function to be invoked after validation / before event; opportunity to modify contents. | 4
221+
`Mapper` | The `IMapper<TSource, TDestination>` override. | 5
222+
`OnEvent` | The action to be invoked once converted to an [`EventData`](../CoreEx/Events/EventData.cs); opportunity to modify contents. | 6
223+
`CreateSuccessResult` | The function to be invoked to create/override the success `IActionResult`. | 7
190224

191225
<br/>
192226

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

199233
### Response
200234

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

203237
<br/>
204238

@@ -218,7 +252,6 @@ public class HttpTriggerQueueVerificationFunction
218252
_settings = settings;
219253
}
220254

221-
[FunctionName(nameof(HttpTriggerQueueVerificationFunction))]
222255
public Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request)
223-
=> _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap());
224-
```
256+
=> _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs<EmployeeVerificationRequest>(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() });
257+
}

src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs

+6-15
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,20 @@ namespace CoreEx.AspNetCore.WebApis
99
/// An attribute that specifies the expected request <b>body</b> <see cref="Type"/> that the action/operation accepts and the supported request content types.
1010
/// </summary>
1111
/// <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>
12+
/// <param name="type">The <b>body</b> <see cref="Type"/>.</param>
13+
/// <param name="contentTypes">The <b>body</b> content type(s). Defaults to <see cref="MediaTypeNames.Application.Json"/>.</param>
14+
/// <exception cref="ArgumentNullException"></exception>
1215
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
13-
public sealed class AcceptsBodyAttribute : Attribute
16+
public sealed class AcceptsBodyAttribute(Type type, params string[] contentTypes) : Attribute
1417
{
15-
/// <summary>
16-
/// Initializes a new instance of the <see cref="AcceptsBodyAttribute"/> class.
17-
/// </summary>
18-
/// <param name="type">The <b>body</b> <see cref="Type"/>.</param>
19-
/// <param name="contentTypes">The <b>body</b> content type(s). Defaults to <see cref="MediaTypeNames.Application.Json"/>.</param>
20-
/// <exception cref="ArgumentNullException"></exception>
21-
public AcceptsBodyAttribute(Type type, params string[] contentTypes)
22-
{
23-
BodyType = type ?? throw new ArgumentNullException(nameof(type));
24-
ContentTypes = contentTypes.Length == 0 ? new string[] { MediaTypeNames.Application.Json } : contentTypes;
25-
}
26-
2718
/// <summary>
2819
/// Gets the <b>body</b> <see cref="Type"/>.
2920
/// </summary>
30-
public Type BodyType { get; }
21+
public Type BodyType { get; } = type.ThrowIfNull(nameof(type));
3122

3223
/// <summary>
3324
/// Gets the <b>body</b> content type(s).
3425
/// </summary>
35-
public string[] ContentTypes { get; }
26+
public string[] ContentTypes { get; } = contentTypes.Length == 0 ? [MediaTypeNames.Application.Json] : contentTypes;
3627
}
3728
}

src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,9 @@ namespace CoreEx.AspNetCore.WebApis
1212
/// <summary>
1313
/// Represents an extended <see cref="StatusCodeResult"/> that enables customization of the <see cref="HttpResponse"/>.
1414
/// </summary>
15-
public class ExtendedStatusCodeResult : StatusCodeResult
15+
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
16+
public class ExtendedStatusCodeResult(HttpStatusCode statusCode) : StatusCodeResult((int)statusCode)
1617
{
17-
/// <summary>
18-
/// Initializes a new instance of the <see cref="ExtendedStatusCodeResult"/> class.
19-
/// </summary>
20-
/// <param name="statusCode">The <see cref="HttpStatusCode"/>.</param>
21-
public ExtendedStatusCodeResult(HttpStatusCode statusCode) : base((int)statusCode) { }
22-
2318
/// <summary>
2419
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders.Location"/> <see cref="Uri"/>.
2520
/// </summary>

0 commit comments

Comments
 (0)