diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ad5156..954c9cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`, `PagingArgs` and `PagingResult` have had `IEquatable` added to enable equality comparisons. diff --git a/Common.targets b/Common.targets index d8136340..7b11ea02 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.9.0 + 3.10.0 preview Avanade Avanade diff --git a/samples/My.Hr/My.Hr.Functions/Functions/HttpTriggerQueueVerificationFunction.cs b/samples/My.Hr/My.Hr.Functions/Functions/HttpTriggerQueueVerificationFunction.cs index e1d88979..56be0417 100644 --- a/samples/My.Hr/My.Hr.Functions/Functions/HttpTriggerQueueVerificationFunction.cs +++ b/samples/My.Hr/My.Hr.Functions/Functions/HttpTriggerQueueVerificationFunction.cs @@ -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 RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request) - => _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap()); + => _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() }); } \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/Abstractions/AspNetCoreServiceCollectionExtensions.cs b/src/CoreEx.AspNetCore/Abstractions/AspNetCoreServiceCollectionExtensions.cs index c1a8b678..a05cde71 100644 --- a/src/CoreEx.AspNetCore/Abstractions/AspNetCoreServiceCollectionExtensions.cs +++ b/src/CoreEx.AspNetCore/Abstractions/AspNetCoreServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static class AspNetCoreServiceCollectionExtensions /// /// Checks that the is not null. /// - private static IServiceCollection CheckServices(IServiceCollection services) => services ?? throw new ArgumentNullException(nameof(services)); + private static IServiceCollection CheckServices(IServiceCollection services) => services.ThrowIfNull(nameof(services)); /// /// Adds the as a scoped service. diff --git a/src/CoreEx.AspNetCore/HealthChecks/HealthService.cs b/src/CoreEx.AspNetCore/HealthChecks/HealthService.cs index a1ab3a69..535fb3de 100644 --- a/src/CoreEx.AspNetCore/HealthChecks/HealthService.cs +++ b/src/CoreEx.AspNetCore/HealthChecks/HealthService.cs @@ -15,21 +15,11 @@ namespace CoreEx.AspNetCore.HealthChecks /// /// Provides the Health Check service. /// - public class HealthService + public class HealthService(SettingsBase settings, HealthCheckService healthCheckService, IJsonSerializer jsonSerializer) { - private readonly SettingsBase _settings; - private readonly HealthCheckService _healthCheckService; - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Initializes a new instance of the class. - /// - 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)); /// /// Runs the health check and returns JSON result. diff --git a/src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs b/src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs index 5c92342a..53e82263 100644 --- a/src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs +++ b/src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs @@ -38,8 +38,7 @@ public static class HttpResultExtensions /// This will automatically invoke where there is an value. 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; @@ -87,8 +86,7 @@ public static HttpRequest ApplyETag(this HttpRequest httpRequest, string? etag) /// The . public static async Task> ReadAsJsonValueAsync(this HttpRequest httpRequest, IJsonSerializer jsonSerializer, bool valueIsRequired = true, IValidator? 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(); @@ -97,7 +95,7 @@ public static async Task> ReadAsJsonValueAsync(this H try { if (content.ToMemory().Length > 0) - jv.Value = (jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer))).Deserialize(content)!; + jv.Value = jsonSerializer.ThrowIfNull(nameof(jsonSerializer)).Deserialize(content)!; if (valueIsRequired && jv.Value == null) jv.ValidationException = new ValidationException($"{InvalidJsonMessagePrefix} Value is mandatory."); @@ -138,7 +136,7 @@ public static async Task> ReadAsJsonValueAsync(this H /// /// The . /// The . - 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))); /// /// Adds the to the . diff --git a/src/CoreEx.AspNetCore/README.md b/src/CoreEx.AspNetCore/README.md index 3250bce7..271ba86e 100644 --- a/src/CoreEx.AspNetCore/README.md +++ b/src/CoreEx.AspNetCore/README.md @@ -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). @@ -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()` | Publish a single message/event with `TValue` being the request content body. -`POST` | `PublishAsync()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem`. +`POST` | `PublishValueAsync()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized). +`POST` | `PublishAsync()` | Publish a single message/event with `TValue` being the request content body mapping to the specified event value type. +`POST` | `PublishValueAsync()` | Publish a single message/event with `TValue` being the specified value (preivously deserialized) mapping to the specified event value type. +- | - +`POST` | `PublishCollectionAsync()` | Publish zero or more message/event(s) from the `TColl` collection with an item type of `TItem` being the request content body. +`POST` | `PublishCollectionValueAsync()` | 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()` | 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()` | 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. + +
+ +### 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`](./WebApis/WebApiPublisherArgsT.cs) - single message with no mapping. +- [`WebApiPublisherArgs`](./WebApis/WebApiPublisherArgsT2.cs) - single message _with_ mapping. +- [`WebApiPublisherCollectionArgs`](./WebApis/WebApiPublisherCollectionArgsT.cs) - collection of messages with no mapping. +- [`WebApiPublisherCollectionArgs`](./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` to validate the request value. | 3 +`OnBeforeEventAsync` | The function to be invoked after validation / before event; opportunity to modify contents. | 4 +`Mapper` | The `IMapper` 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
@@ -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.
@@ -218,7 +252,6 @@ public class HttpTriggerQueueVerificationFunction _settings = settings; } - [FunctionName(nameof(HttpTriggerQueueVerificationFunction))] public Task RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "employee/verify")] HttpRequest request) - => _webApiPublisher.PublishAsync(request, _settings.VerificationQueueName, validator: new EmployeeVerificationValidator().Wrap()); -``` + => _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs(_settings.VerificationQueueName) { Validator = new EmployeeVerificationValidator().Wrap() }); +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs b/src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs index 5f9031f5..1813c122 100644 --- a/src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs +++ b/src/CoreEx.AspNetCore/WebApis/AcceptsBodyAttribute.cs @@ -9,29 +9,20 @@ namespace CoreEx.AspNetCore.WebApis /// An attribute that specifies the expected request body that the action/operation accepts and the supported request content types. ///
/// The is used to enable Swagger/Swashbuckle generated documentation where the operation does not explicitly define the body as a method parameter; i.e. via . + /// The body . + /// The body content type(s). Defaults to . + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class AcceptsBodyAttribute : Attribute + public sealed class AcceptsBodyAttribute(Type type, params string[] contentTypes) : Attribute { - /// - /// Initializes a new instance of the class. - /// - /// The body . - /// The body content type(s). Defaults to . - /// - public AcceptsBodyAttribute(Type type, params string[] contentTypes) - { - BodyType = type ?? throw new ArgumentNullException(nameof(type)); - ContentTypes = contentTypes.Length == 0 ? new string[] { MediaTypeNames.Application.Json } : contentTypes; - } - /// /// Gets the body . /// - public Type BodyType { get; } + public Type BodyType { get; } = type.ThrowIfNull(nameof(type)); /// /// Gets the body content type(s). /// - public string[] ContentTypes { get; } + public string[] ContentTypes { get; } = contentTypes.Length == 0 ? [MediaTypeNames.Application.Json] : contentTypes; } } \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs b/src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs index ce663243..1b40a6ce 100644 --- a/src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs +++ b/src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs @@ -12,14 +12,9 @@ namespace CoreEx.AspNetCore.WebApis /// /// Represents an extended that enables customization of the . /// - public class ExtendedStatusCodeResult : StatusCodeResult + /// The . + public class ExtendedStatusCodeResult(HttpStatusCode statusCode) : StatusCodeResult((int)statusCode) { - /// - /// Initializes a new instance of the class. - /// - /// The . - public ExtendedStatusCodeResult(HttpStatusCode statusCode) : base((int)statusCode) { } - /// /// Gets or sets the . /// diff --git a/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherArgs.cs b/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherArgs.cs new file mode 100644 index 00000000..a1999770 --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherArgs.cs @@ -0,0 +1,80 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Enables the arguments. + /// + /// The request JSON content value . + /// The (where different then a will be required). + public interface IWebApiPublisherArgs + { + /// + /// Indicates whether the and are the same . + /// + internal bool AreSameType => typeof(TValue) == typeof(TEventValue); + + /// + /// Gets or sets the optional event destintion name (e.g. Queue or Topic name). + /// + /// Will leverage either or depending on whether name is specified or not. + string? EventName { get; } + + /// + /// Gets or sets the where successful. + /// + /// Defaults to . + HttpStatusCode StatusCode { get; } + + /// + /// Gets or sets the optional validator. + /// + IValidator? Validator { get; } + + /// + /// Gets or sets the . + /// + /// Defaults to . + OperationType OperationType { get; } + + /// + /// Gets or sets the on before validation modifier function. + /// + /// Enables the value to be modified before validation. The will allow failures and alike to be returned where applicable. + Func, CancellationToken, Task>? OnBeforeValidationAsync { get; } + + /// + /// Gets or sets the after validation / on before event modifier function. + /// + /// Enables the value to be modified after validation. The will allow failures and alike to be returned where applicable. + Func, CancellationToken, Task>? OnBeforeEventAsync { get; } + + /// + /// Gets or sets the modifier function. + /// + /// Enables the corresponding to be modified prior to publish. + Action? OnEvent { get; } + + /// + /// Gets or sets the to override. + /// + /// Where null the will be used to get the corresponding instance to perform the underlying mapping. + IMapper? Mapper { get; } + + /// + /// Gets or sets the function to override the creation of the success . + /// + /// Defaults to a using the defined . + Func? CreateSuccessResult { get; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherCollectionArgs.cs b/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherCollectionArgs.cs new file mode 100644 index 00000000..40f46678 --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/IWebApiPublisherCollectionArgs.cs @@ -0,0 +1,89 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Configuration; +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Enables the collection-based arguments. + /// + /// The request JSON collection . + /// The collection item . + /// The -equivalent (where different then a will be required). + public interface IWebApiPublisherCollectionArgs where TColl : IEnumerable + { + /// + /// Indicates whether the and are the same . + /// + internal bool AreSameType => typeof(TItem) == typeof(TEventItem); + + /// + /// Gets or sets the optional event destintion name (e.g. Queue or Topic name). + /// + /// Will leverage either or depending on whether name is specified or not. + string? EventName { get; } + + /// + /// Gets or sets the where successful. + /// + /// Defaults to . + HttpStatusCode StatusCode { get; } + + /// + /// Gets or sets the maximum collection size. + /// + /// Defaults to . + int? MaxCollectionSize { get; } + + /// + /// Gets or sets the optional validator + /// + IValidator? Validator { get; } + + /// + /// Gets or sets the . + /// + /// Defaults to . + OperationType OperationType { get; } + + /// + /// Gets or sets the on before validation modifier function. + /// + /// Enables the value to be modified before validation. The will allow failures and alike to be returned where applicable. + Func, CancellationToken, Task>? OnBeforeValidationAsync { get; } + + /// + /// Gets or sets the after validation / on before event modifier function. + /// + /// Enables the value to be modified after validation. The will allow failures and alike to be returned where applicable. + Func, CancellationToken, Task>? OnBeforeEventAsync { get; } + + /// + /// Gets or sets the modifier function. + /// + /// Enables the corresponding to be modified prior to publish. + Action? OnEvent { get; } + + /// + /// Gets or sets the to override. + /// + /// Where null the will be used to get the corresponding instance to perform the underlying mapping. + IMapper? Mapper { get; } + + /// + /// Gets or sets the function to override the creation of the success . + /// + /// Defaults to a using the defined . + Func? CreateSuccessResult { get; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/ReferenceDataContentWebApi.cs b/src/CoreEx.AspNetCore/WebApis/ReferenceDataContentWebApi.cs index 3d056397..9911a55a 100644 --- a/src/CoreEx.AspNetCore/WebApis/ReferenceDataContentWebApi.cs +++ b/src/CoreEx.AspNetCore/WebApis/ReferenceDataContentWebApi.cs @@ -13,18 +13,12 @@ namespace CoreEx.AspNetCore.WebApis /// Provides the core (, , and ) Web API execution encapsulation that uses the /// to allow types to serialize contents. ///
- public class ReferenceDataContentWebApi : WebApi - { - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The . - /// The . - /// The ; defaults where not specified. - /// The to support the operations. - public ReferenceDataContentWebApi(ExecutionContext executionContext, SettingsBase settings, IReferenceDataContentJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker = null, IJsonMergePatch? jsonMergePatch = null) - : base(executionContext, settings, jsonSerializer, logger, invoker, jsonMergePatch) { } - } + /// The . + /// The . + /// The . + /// The . + /// The ; defaults where not specified. + /// The to support the operations. + public class ReferenceDataContentWebApi(ExecutionContext executionContext, SettingsBase settings, IReferenceDataContentJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker = null, IJsonMergePatch? jsonMergePatch = null) + : WebApi(executionContext, settings, jsonSerializer, logger, invoker, jsonMergePatch) { } } \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs b/src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs index 92993861..c65f4096 100644 --- a/src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs +++ b/src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs @@ -34,7 +34,7 @@ public sealed class ValueContentResult : ExtendedContentResult /// The related . /// The related . /// The . - private ValueContentResult(string content, HttpStatusCode statusCode, string? etag, PagingResult? pagingResult, Uri? location) + public ValueContentResult(string content, HttpStatusCode statusCode, string? etag, PagingResult? pagingResult, Uri? location) { Content = content; ContentType = MediaTypeNames.Application.Json; diff --git a/src/CoreEx.AspNetCore/WebApis/WebApi.cs b/src/CoreEx.AspNetCore/WebApis/WebApi.cs index 71c0a9db..ed37f674 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApi.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApi.cs @@ -90,11 +90,8 @@ public Task RunAsync(HttpRequest request, Func RunAsync(HttpRequest request, Func, CancellationToken, Task> function, OperationType operationType = OperationType.Unspecified, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (function == null) - throw new ArgumentNullException(nameof(function)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); return await RunAsync(request, async (wap, ct) => { @@ -136,15 +133,12 @@ public Task GetAsync(HttpRequest request, Func GetAsync(HttpRequest request, Func> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NotFound, OperationType operationType = OperationType.Read, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsGet(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Get}' to use {nameof(GetAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -180,15 +174,12 @@ public Task PostAsync(HttpRequest request, Func PostAsync(HttpRequest request, Func function, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Create, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { await function(wap, ct).ConfigureAwait(false); @@ -270,15 +261,12 @@ public Task PostAsync(HttpRequest request, TValue value, private async Task PostInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Create, bool valueIsRequired = true, IValidator? validator = null, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -320,15 +308,12 @@ public Task PostAsync(HttpRequest request, Func PostAsync(HttpRequest request, Func> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Create, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -418,15 +403,12 @@ public Task PostAsync(HttpRequest request, TValu private async Task PostInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Create, bool valueIsRequired = true, IValidator? validator = null, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -512,15 +494,12 @@ public Task PutAsync(HttpRequest request, TValue value, F private async Task PutInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Update, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -610,15 +589,12 @@ public Task PutAsync(HttpRequest request, TValue public async Task PutInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Update, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -704,18 +680,13 @@ public Task PutAsync(HttpRequest request, TValue value, F private async Task PutInternalAsync(HttpRequest request, bool useValue, TValue value, Func> get, Func, CancellationToken, Task> put, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Update, IValidator? validator = null, bool simulatedConcurrency = false, CancellationToken cancellationToken = default) where TValue : class { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + get.ThrowIfNull(nameof(get)); + put.ThrowIfNull(nameof(put)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutAsync)}.", nameof(request)); - if (get == null) - throw new ArgumentNullException(nameof(get)); - - if (put == null) - throw new ArgumentNullException(nameof(put)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, cancellationToken).ConfigureAwait(false); @@ -785,15 +756,12 @@ public Task DeleteAsync(HttpRequest request, FuncThe corresponding where successful. public async Task DeleteAsync(HttpRequest request, Func function, HttpStatusCode statusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Delete, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsDelete(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Delete}' to use {nameof(DeleteAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { try @@ -844,21 +812,16 @@ public Task PatchAsync(HttpRequest request, Func PatchAsync(HttpRequest request, Func> get, Func, CancellationToken, Task> put, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Update, IValidator? validator = null, bool simulatedConcurrency = false, CancellationToken cancellationToken = default) where TValue : class { + request.ThrowIfNull(nameof(request)); + get.ThrowIfNull(nameof(get)); + put.ThrowIfNull(nameof(put)); + if (JsonMergePatch == null) throw new InvalidOperationException($"To use the '{nameof(PatchAsync)}' methods the '{nameof(JsonMergePatch)}' object must be passed in the constructor. Where using dependency injection consider using '{nameof(Microsoft.Extensions.DependencyInjection.IServiceCollectionExtensions.AddJsonMergePatch)}' to add and configure the supported options."); - if (request == null) - throw new ArgumentNullException(nameof(request)); - if (!HttpMethods.IsPatch(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Patch}' to use {nameof(PatchAsync)}.", nameof(request)); - if (get == null) - throw new ArgumentNullException(nameof(get)); - - if (put == null) - throw new ArgumentNullException(nameof(put)); - return await RunAsync(request, async (wap, ct) => { // Make sure that the only the support content types are used. diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs b/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs index 55238d33..6c3851bf 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiBase.cs @@ -25,49 +25,37 @@ namespace CoreEx.AspNetCore.WebApis /// /// Provides the base Web API execution encapsulation to the underlying logic in a consistent manner. /// - public abstract class WebApiBase + /// The . + /// The . + /// The . + /// The . + /// The ; defaults where not specified. + public abstract class WebApiBase(ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker) { - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The . - /// The . - /// The ; defaults where not specified. - protected WebApiBase(ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker) - { - ExecutionContext = executionContext ?? throw new ArgumentNullException(nameof(executionContext)); - Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - JsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Invoker = invoker ?? WebApiInvoker.Current; - } - /// /// Gets the . /// - public ExecutionContext ExecutionContext { get; } + public ExecutionContext ExecutionContext { get; } = executionContext.ThrowIfNull(nameof(executionContext)); /// /// Gets the . /// - public SettingsBase Settings { get; } + public SettingsBase Settings { get; } = settings.ThrowIfNull(nameof(settings)); /// /// Gets the . /// - public IJsonSerializer JsonSerializer { get; } + public IJsonSerializer JsonSerializer { get; } = jsonSerializer.ThrowIfNull(nameof(jsonSerializer)); /// /// Gets the . /// - public ILogger Logger { get; } + public ILogger Logger { get; } = logger.ThrowIfNull(nameof(logger)); /// /// Gets the . /// - public WebApiInvoker Invoker { get; } + public WebApiInvoker Invoker { get; } = invoker ?? WebApiInvoker.Current; /// /// Gets or sets the list of secondary correlation identifier names. @@ -98,11 +86,8 @@ public virtual IEnumerable GetCorrelationIdNames() /// This is, and must be, used by all methods that process an to ensure that the standardized before and after, success and error, handling occurs as required. protected async Task RunAsync(HttpRequest request, Func> function, OperationType operationType = OperationType.Unspecified, CancellationToken cancellationToken = default, [CallerMemberName] string? memberName = null) { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (function == null) - throw new ArgumentNullException(nameof(function)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); // Invoke the "actual" function via the pluggable invoker. ExecutionContext.OperationType = operationType; @@ -123,8 +108,7 @@ protected async Task RunAsync(HttpRequest request, FuncThe where there is an error; otherwise, for success. protected internal async Task<(WebApiParam?, Exception?)> ValidateValueAsync(WebApiParam wap, bool useValue, TValue value, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (wap == null) - throw new ArgumentNullException(nameof(wap)); + wap.ThrowIfNull(nameof(wap)); WebApiParam wapv; if (useValue) @@ -181,10 +165,10 @@ protected async Task RunAsync(HttpRequest request, FuncThe corresponding . public static async Task CreateActionResultFromExceptionAsync(WebApiBase? owner, HttpContext context, Exception exception, SettingsBase settings, ILogger logger, Func>? unhandledExceptionAsync = null, CancellationToken cancellationToken = default) { - if (context is null) throw new ArgumentNullException(nameof(context)); - if (exception is null) throw new ArgumentNullException(nameof(exception)); - if (logger is null) throw new ArgumentNullException(nameof(logger)); - if (settings is null) throw new ArgumentNullException(nameof(settings)); + context.ThrowIfNull(nameof(context)); + exception.ThrowIfNull(nameof(exception)); + logger.ThrowIfNull(nameof(logger)); + settings.ThrowIfNull(nameof(settings)); if (owner is not null && !owner.Invoker.CatchAndHandleExceptions) throw exception; diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiExceptionHandlerMiddleware.cs b/src/CoreEx.AspNetCore/WebApis/WebApiExceptionHandlerMiddleware.cs index d5daa478..d473be04 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiExceptionHandlerMiddleware.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiExceptionHandlerMiddleware.cs @@ -15,25 +15,14 @@ namespace CoreEx.AspNetCore.WebApis /// /// Provides a CoreEx oriented handling middleware that is result aware. /// - public class WebApiExceptionHandlerMiddleware + /// The next . + /// The . + /// The . + public class WebApiExceptionHandlerMiddleware(RequestDelegate next, SettingsBase settings, ILogger logger) { - private readonly RequestDelegate _next; - private readonly SettingsBase _settings; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the . - /// - /// The next . - /// The . - /// The . - /// - public WebApiExceptionHandlerMiddleware(RequestDelegate next, SettingsBase settings, ILogger logger) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } + private readonly RequestDelegate _next = next.ThrowIfNull(nameof(next)); + private readonly SettingsBase _settings = settings.ThrowIfNull(nameof(settings)); + private readonly ILogger _logger = logger.ThrowIfNull(nameof(logger)); /// /// Invokes the . diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiExecutionContextMiddleware.cs b/src/CoreEx.AspNetCore/WebApis/WebApiExecutionContextMiddleware.cs index e85f7e50..882c36e3 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiExecutionContextMiddleware.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiExecutionContextMiddleware.cs @@ -11,10 +11,12 @@ namespace CoreEx.AspNetCore.WebApis /// Provides an handling middleware that (using dependency injection) enables additional configuration where required. /// /// A new is instantiated through dependency injection using the . - public class WebApiExecutionContextMiddleware + /// The next . + /// The optional function to update the . Defaults to where not specified. + public class WebApiExecutionContextMiddleware(RequestDelegate next, Func? executionContextUpdate = null) { - private readonly RequestDelegate _next; - private readonly Func _updateFunc; + private readonly RequestDelegate _next = next.ThrowIfNull(nameof(next)); + private readonly Func _updateFunc = executionContextUpdate ?? DefaultExecutionContextUpdate; /// /// Gets the default username where it is unable to be inferred ( from the ). @@ -30,28 +32,14 @@ public class WebApiExecutionContextMiddleware /// The will be set to the from the ; otherwise, where null. public static Task DefaultExecutionContextUpdate(HttpContext context, ExecutionContext ec) { - if (context == null) - throw new ArgumentNullException(nameof(context)); - - if (ec == null) - throw new ArgumentNullException(nameof(ec)); + context.ThrowIfNull(nameof(context)); + ec.ThrowIfNull(nameof(ec)); ec.UserName = context.User?.Identity?.Name ?? DefaultUsername; ec.Timestamp = context.RequestServices.GetService()?.UtcNow ?? SystemTime.Default.UtcNow; return Task.CompletedTask; } - /// - /// Initializes a new instance of the . - /// - /// The next . - /// The optional function to update the . Defaults to where not specified. - public WebApiExecutionContextMiddleware(RequestDelegate next, Func? executionContextUpdate = null) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _updateFunc = executionContextUpdate ?? DefaultExecutionContextUpdate; - } - /// /// Invokes the . /// @@ -59,8 +47,7 @@ public WebApiExecutionContextMiddleware(RequestDelegate next, FuncThe . public async Task InvokeAsync(HttpContext context) { - if (context == null) - throw new ArgumentNullException(nameof(context)); + context.ThrowIfNull(nameof(context)); var ec = context.RequestServices.GetRequiredService(); ec.ServiceProvider ??= context.RequestServices; diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiParam.cs b/src/CoreEx.AspNetCore/WebApis/WebApiParam.cs index c1a662ce..4c003d3c 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiParam.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiParam.cs @@ -9,25 +9,15 @@ namespace CoreEx.AspNetCore.WebApis /// /// Represents a parameter. /// - public class WebApiParam + /// The parent instance. + /// The . + /// The . + public class WebApiParam(WebApiBase webApi, WebApiRequestOptions requestOptions, OperationType operationType = OperationType.Unspecified) { - /// - /// Initializes a new instance of the class. - /// - /// The parent instance. - /// The . - /// The . - public WebApiParam(WebApiBase webApi, WebApiRequestOptions requestOptions, OperationType operationType = OperationType.Unspecified) - { - WebApi = webApi ?? throw new ArgumentNullException(nameof(webApi)); - RequestOptions = requestOptions ?? throw new ArgumentNullException(nameof(requestOptions)); - OperationType = operationType; - } - /// /// Gets the parent (invoking) . /// - public WebApiBase WebApi { get; } + public WebApiBase WebApi { get; } = webApi.ThrowIfNull(nameof(webApi)); /// /// Gets or sets the . @@ -37,7 +27,7 @@ public WebApiParam(WebApiBase webApi, WebApiRequestOptions requestOptions, Opera /// /// Gets or sets the . /// - public WebApiRequestOptions RequestOptions { get; } + public WebApiRequestOptions RequestOptions { get; } = requestOptions.ThrowIfNull(nameof(requestOptions)); /// /// Gets the . @@ -47,7 +37,7 @@ public WebApiParam(WebApiBase webApi, WebApiRequestOptions requestOptions, Opera /// /// Gets the . /// - public OperationType OperationType { get; } + public OperationType OperationType { get; } = operationType; /// /// Inspects the to either update the or where appropriate. diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiParamT.cs b/src/CoreEx.AspNetCore/WebApis/WebApiParamT.cs index 59d4f70f..d18e7539 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiParamT.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiParamT.cs @@ -1,7 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx -using System; - namespace CoreEx.AspNetCore.WebApis { /// @@ -14,7 +12,7 @@ public class WebApiParam : WebApiParam /// /// The to copy from. /// The deserialized request value. - public WebApiParam(WebApiParam wap, T value) : base((wap ?? throw new ArgumentNullException(nameof(wap))).WebApi, wap.RequestOptions, wap.OperationType) => Value = InspectValue(value); + public WebApiParam(WebApiParam wap, T value) : base(wap.ThrowIfNull(nameof(wap)).WebApi, wap.RequestOptions, wap.OperationType) => Value = InspectValue(value); /// /// Gets the deserialized request value. diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiPublisher.cs b/src/CoreEx.AspNetCore/WebApis/WebApiPublisher.cs index 5b54587a..a9975942 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiPublisher.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiPublisher.cs @@ -5,14 +5,12 @@ using CoreEx.Json; using CoreEx.Mapping; using CoreEx.Results; -using CoreEx.Validation; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; @@ -22,26 +20,20 @@ namespace CoreEx.AspNetCore.WebApis /// Provides the core Web API execution encapsulation. /// /// Support to change/map request into a different published event type is also enabled where required (see also ). - public class WebApiPublisher : WebApiBase + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + public class WebApiPublisher(IEventPublisher eventPublisher, ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker = null) : WebApiBase(executionContext, settings, jsonSerializer, logger, invoker) { private IMapper? _mapper; - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The . - /// The . - /// The . - /// The . - public WebApiPublisher(IEventPublisher eventPublisher, ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger logger, WebApiInvoker? invoker = null) - : base(executionContext, settings, jsonSerializer, logger, invoker) => EventPublisher = eventPublisher ?? throw new ArgumentNullException(nameof(eventPublisher)); - /// /// Gets the . /// - public IEventPublisher EventPublisher { get; } + public IEventPublisher EventPublisher { get; } = eventPublisher.ThrowIfNull(nameof(eventPublisher)); /// /// Gets or sets the . @@ -62,48 +54,25 @@ public IMapper Mapper /// /// The request JSON content value . /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The . /// The . /// The corresponding where successful. /// The must have an of . - public Task PublishAsync(HttpRequest request, string? eventName, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishAsync(request, eventName, (wapv, ct) => PublishBeforeEventAsync(wapv, beforeEvent, ct), eventModifier, statusCode, operationType, validator, cancellationToken); + public Task PublishAsync(HttpRequest request, WebApiPublisherArgs? args = null, CancellationToken cancellationToken = default) + => PublishOrchestrateAsync(request, false, default!, args ?? new WebApiPublisherArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of that is to be published using the . /// /// The request JSON content value . /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The value (already deserialized). + /// The . /// The . /// The corresponding where successful. /// The must have an of . - public Task PublishAsync(HttpRequest request, TValue value, string? eventName = null, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishAsync(request, value, eventName, (wapv, ct) => PublishBeforeEventAsync(wapv, beforeEvent, ct), eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Simulates a before event execution; where it in turn maps to itself. - /// - private static async Task PublishBeforeEventAsync(WebApiParam wapv, Func, CancellationToken, Task>? beforeEvent, CancellationToken cancellationToken) - { - if (beforeEvent is not null) - await beforeEvent(wapv, cancellationToken).ConfigureAwait(false); - - return wapv.Value is TEventValue tev ? tev : throw new InvalidCastException("The TValue and TEventValue must be the same Type."); - } + public Task PublishValueAsync(HttpRequest request, TValue value, WebApiPublisherArgs? args = null, CancellationToken cancellationToken = default) + => PublishOrchestrateAsync(request, true, value, args ?? new WebApiPublisherArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of that is to be published using the . @@ -111,58 +80,12 @@ private static async Task PublishBeforeEventAsyncThe request JSON content value . /// The (where different to the request). /// The . - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The . /// The . /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishAsync(HttpRequest request, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishAsync(request, null, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the . - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishAsync(HttpRequest request, string? eventName, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishInternalAsync(request, false, default!, eventName, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the . - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// The The value (already deserialized). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishAsync(HttpRequest request, TValue value, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishAsync(request, value, null, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); + /// The must have an of . + public Task PublishAsync(HttpRequest request, WebApiPublisherArgs? args = null, CancellationToken cancellationToken = default) + => PublishOrchestrateAsync(request, false, default!, args ?? new WebApiPublisherArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of that is to be published using the . @@ -170,55 +93,47 @@ public Task PublishAsync(HttpRequest request /// The request JSON content value . /// The (where different to the request). /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The value (already deserialized). + /// The . /// The . /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishAsync(HttpRequest request, TValue value, string? eventName, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishInternalAsync(request, true, value, eventName, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); + /// The must have an of . + public Task PublishValueAsync(HttpRequest request, TValue value, WebApiPublisherArgs? args = null, CancellationToken cancellationToken = default) + => PublishOrchestrateAsync(request, true, value, args ?? new WebApiPublisherArgs(), cancellationToken); /// - /// Performs a operation with a request JSON content value of that is to be published using the . + /// Performs the publish orchestration. /// - private async Task PublishInternalAsync(HttpRequest request, bool useValue, TValue value, string? eventName, Func, CancellationToken, Task>? beforeEvent, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) + private async Task PublishOrchestrateAsync(HttpRequest request, bool useValue, TValue value, IWebApiPublisherArgs args, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(); + args.ThrowIfNull(); if (request.Method != HttpMethods.Post) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PublishAsync)}.", nameof(request)); return await RunAsync(request, async (wap, ct) => { - var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, cancellationToken).ConfigureAwait(false); - if (vex != null) + // Use specified value or get from the request. + var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, null, cancellationToken).ConfigureAwait(false); + if (vex is not null) return await CreateActionResultFromExceptionAsync(this, request.HttpContext, vex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - var @event = new EventData - { - Value = beforeEvent is null ? Mapper.Map(wapv!.Value) : await beforeEvent(wapv!, ct).ConfigureAwait(false) - }; - - eventModifier?.Invoke(@event); - - if (eventName == null) - EventPublisher.Publish(@event); - else - EventPublisher.PublishNamed(eventName, @event); - - await EventPublisher.SendAsync(cancellationToken).ConfigureAwait(false); - - return new ExtendedStatusCodeResult(statusCode); - }, operationType, cancellationToken, nameof(PublishAsync)).ConfigureAwait(false); + // Process the publishing as configured. + var r = await Result.Go() + .WhenAsAsync(() => args.OnBeforeValidationAsync is not null, () => args.OnBeforeValidationAsync!(wapv!, ct)) + .WhenAsAsync(_ => args.Validator is not null, async _ => (await args.Validator!.ValidateAsync(wapv!.Value!, ct).ConfigureAwait(false)).ToResult()) + .WhenAsync(_ => args.OnBeforeEventAsync is not null, _ => args.OnBeforeEventAsync!(wapv!, ct)) + .WhenAs(_ => args.AreSameType, _ => new EventData { Value = wapv!.Value }, _ => new EventData { Value = args.Mapper is not null ? args.Mapper.Map(wapv!.Value) : Mapper.Map(wapv!.Value) }) + .Then(e => args.OnEvent?.Invoke(e)) + .ThenAs(e => args.EventName is null ? EventPublisher.Publish(e) : EventPublisher.PublishNamed(args.EventName, e)) + .ThenAsync(_ => EventPublisher.SendAsync(ct)).ConfigureAwait(false); + + if (r.IsFailure) + return await CreateActionResultFromExceptionAsync(this, request.HttpContext, r.Error, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); + + return args.CreateSuccessResult?.Invoke() ?? new ExtendedStatusCodeResult(args.StatusCode); + }, args.OperationType, cancellationToken, nameof(PublishAsync)).ConfigureAwait(false); } #endregion @@ -228,172 +143,82 @@ private async Task PublishInternalAsync(Http /// /// Performs a operation with a request JSON content value of where each item is to be published using the . /// - /// The collection + /// The request JSON collection . /// The collection item . /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The . /// The . /// The corresponding where successful. /// The must have an of . - public Task PublishCollectionAsync(HttpRequest request, string? eventName = null, Func, CancellationToken, Task>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionAsync(request, eventName, (wapc, ct) => PublishCollectionBeforeEventAsync(wapc, beforeEvents, ct), eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); + public Task PublishCollectionAsync(HttpRequest request, WebApiPublisherCollectionArgs? args = null, CancellationToken cancellationToken = default) where TColl : IEnumerable + => PublishCollectionOrchestrateAsync(request, false, default!, args ?? new WebApiPublisherCollectionArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of where each item is to be published using the . /// - /// The collection + /// The request JSON collection . /// The collection item . /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The value (already deserialized). + /// The . /// The . /// The corresponding where successful. /// The must have an of . - public Task PublishCollectionAsync(HttpRequest request, TColl value, string? eventName = null, Func, CancellationToken, Task>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionAsync(request, value, eventName, (wapc, ct) => PublishCollectionBeforeEventAsync(wapc, beforeEvents, ct), eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Simulates a before event execution; where it in turn maps to itself. - /// - private static async Task> PublishCollectionBeforeEventAsync(WebApiParam wapc, Func, CancellationToken, Task>? beforeEvent, CancellationToken cancellationToken) where TColl : IEnumerable - { - if (beforeEvent is not null) - await beforeEvent(wapc, cancellationToken).ConfigureAwait(false); - - var items = new List(); - foreach (var item in wapc.Value!) - { - if (item is TEventItem tei) - items.Add(tei); - else - throw new InvalidCastException("The TItem and TEventItem must be the same Type."); - } - - return items; - } - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the . - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishCollectionAsync(HttpRequest request, Func, CancellationToken, Task>>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionAsync(request, null, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the . - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishCollectionAsync(HttpRequest request, string? eventName, Func, CancellationToken, Task>>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionInternalAsync(request, false, default!, eventName, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); + public Task PublishCollectionValueAsync(HttpRequest request, TColl value, WebApiPublisherCollectionArgs? args = null, CancellationToken cancellationToken = default) where TColl : IEnumerable + => PublishCollectionOrchestrateAsync(request, true, value, args ?? new WebApiPublisherCollectionArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of where each item is to be published using the . /// - /// The collection + /// The request JSON collection . /// The collection item . - /// The item (where different to the request). + /// The -equivalent (where different to the request). /// The . - /// The The value (already deserialized). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The . /// The . /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishCollectionAsync(HttpRequest request, TColl value, Func, CancellationToken, Task>>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionAsync(request, value, null, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); + /// The must have an of . + public Task PublishCollectionAsync(HttpRequest request, WebApiPublisherCollectionArgs? args = null, CancellationToken cancellationToken = default) where TColl : IEnumerable + => PublishCollectionOrchestrateAsync(request, false, default!, args ?? new WebApiPublisherCollectionArgs(), cancellationToken); /// /// Performs a operation with a request JSON content value of where each item is to be published using the . /// - /// The collection + /// The request JSON collection . /// The collection item . - /// The item (where different to the request). + /// The -equivalent (where different to the request). /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted (invoked after the ). - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. + /// The value (already deserialized). + /// The . /// The . /// The corresponding where successful. - /// The must have an of . - /// Where the is null then the will be used to map between the and types. - public Task PublishCollectionAsync(HttpRequest request, TColl value, string? eventName, Func, CancellationToken, Task>>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionInternalAsync(request, true, value, eventName, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); + /// The must have an of . + public Task PublishCollectionValueAsync(HttpRequest request, TColl value, WebApiPublisherCollectionArgs? args = null, CancellationToken cancellationToken = default) where TColl : IEnumerable + => PublishCollectionOrchestrateAsync(request, true, value, args ?? new WebApiPublisherCollectionArgs(), cancellationToken); /// - /// Performs a operation with a request JSON content value of where each item is to be published using the . + /// Performs the publish orchestration. /// - private async Task PublishCollectionInternalAsync(HttpRequest request, bool useValue, TColl value, string? eventName, Func, CancellationToken, Task>>? beforeEvents, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable + private async Task PublishCollectionOrchestrateAsync(HttpRequest request, bool useValue, TColl coll, IWebApiPublisherCollectionArgs args, CancellationToken cancellationToken = default) where TColl : IEnumerable { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(); + args.ThrowIfNull(); if (request.Method != HttpMethods.Post) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PublishAsync)}.", nameof(request)); - // Fall back to a mapper where no explicit beforeEvents is specified. return await RunAsync(request, async (wap, ct) => { - var (wapc, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, cancellationToken).ConfigureAwait(false); - if (vex != null) + // Use specified value or get from the request. + var (wapc, vex) = await ValidateValueAsync(wap, useValue, coll, true, null, cancellationToken).ConfigureAwait(false); + if (vex is not null) return await CreateActionResultFromExceptionAsync(this, request.HttpContext, vex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - var max = maxCollSize ?? Settings.MaxPublishCollSize; + // Check the collection size. + var max = args.MaxCollectionSize ?? Settings.MaxPublishCollSize; + if (max <= 0) + throw new InvalidOperationException($"The maximum collection size must be greater than zero."); + var count = wapc!.Value?.Count() ?? 0; if (count > max) { @@ -405,403 +230,31 @@ private async Task PublishCollectionInternalAsync items; - if (beforeEvents is null) - { - var coll = new List(); - { - foreach (var item in wapc.Value!) - { - coll.Add(Mapper.Map(item)!); - } - } - - items = coll.AsEnumerable(); - } - else - items = await beforeEvents(wapc!, ct).ConfigureAwait(false); - - foreach (var item in items) - { - var @event = new EventData { Value = item }; - - eventModifier?.Invoke(@event); - - if (eventName == null) - EventPublisher.Publish(@event); - else - EventPublisher.PublishNamed(eventName, @event); - } - - await EventPublisher.SendAsync(ct).ConfigureAwait(false); - - return new ExtendedStatusCodeResult(statusCode); - }, operationType, cancellationToken, nameof(PublishAsync)).ConfigureAwait(false); - } - - #endregion - - #region PublishWithResultAsync - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, string? eventName = null, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultAsync(request, eventName, (wapv, ct) => PublishBeforeEventWithResultAsync(wapv, beforeEvent, ct), eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, TValue value, string? eventName = null, Func, CancellationToken, Task>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultAsync(request, value, eventName, (wapv, ct) => PublishBeforeEventWithResultAsync(wapv, beforeEvent, ct), eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Simulates a before event execution; where it in turn maps to itself. - /// - private static async Task> PublishBeforeEventWithResultAsync(WebApiParam wapv, Func, CancellationToken, Task>? beforeEvent, CancellationToken cancellationToken) - { - var result = beforeEvent is null ? Result.Success : await beforeEvent(wapv, cancellationToken).ConfigureAwait(false); - return result.ThenAs(() => wapv.Value is TEventValue tev ? tev : throw new InvalidCastException("The TValue and TEventValue must be the same Type.")); - } - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, Func, CancellationToken, Task>> beforeEvent, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultAsync(request, null, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, string? eventName, Func, CancellationToken, Task>> beforeEvent, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultInternalAsync(request, false, default!, eventName, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// The The value (already deserialized). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, TValue value, Func, CancellationToken, Task>> beforeEvent, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultAsync(request, value, null, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - /// The request JSON content value . - /// The (where different to the request). - /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the value to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable the instance to be modified prior to publish. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishWithResultAsync(HttpRequest request, TValue value, string? eventName, Func, CancellationToken, Task>> beforeEvent, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - => PublishWithResultInternalAsync(request, true, value, eventName, beforeEvent, eventModifier, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of that is to be published using the (with a ). - /// - private async Task PublishWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, string? eventName, Func, CancellationToken, Task>>? beforeEvent = null, Action? eventModifier = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) - { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (beforeEvent is null) - throw new ArgumentNullException(nameof(beforeEvent)); - - if (request.Method != HttpMethods.Post) - throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PublishWithResultAsync)}.", nameof(request)); - - return await RunAsync(request, async (wap, ct) => - { - var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, cancellationToken).ConfigureAwait(false); - if (vex != null) - return await CreateActionResultFromExceptionAsync(this, request.HttpContext, vex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - - var result = await beforeEvent(wapv!, ct).ConfigureAwait(false); - if (result.IsFailure) - return await CreateActionResultFromExceptionAsync(this, request.HttpContext, result.Error, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - - var @event = new EventData { Value = result.Value }; - eventModifier?.Invoke(@event); - - if (eventName == null) - EventPublisher.Publish(@event); - else - EventPublisher.PublishNamed(eventName, @event); - - await EventPublisher.SendAsync(cancellationToken).ConfigureAwait(false); - - return new ExtendedStatusCodeResult(statusCode); - }, operationType, cancellationToken, nameof(PublishWithResultAsync)).ConfigureAwait(false); - } - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, string? eventName = null, Func, CancellationToken, Task>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultAsync(request, eventName, (wapc, ct) => PublishCollectionBeforeEventWithResultAsync(wapc, beforeEvents, ct), eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, TColl value, string? eventName = null, Func, CancellationToken, Task>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultAsync(request, value, eventName, (wapc, ct) => PublishCollectionBeforeEventWithResultAsync(wapc, beforeEvents, ct), eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Simulates a before event execution; where it in turn maps to itself. - /// - private static async Task>> PublishCollectionBeforeEventWithResultAsync(WebApiParam wapc, Func, CancellationToken, Task>? beforeEvent, CancellationToken cancellationToken) where TColl : IEnumerable - { - var result = beforeEvent is null ? Result.Success : await beforeEvent(wapc, cancellationToken).ConfigureAwait(false); - return result.ThenAs(() => - { - var items = new List(); - foreach (var item in wapc.Value!) - { - if (item is TEventItem tei) - items.Add(tei); - else - throw new InvalidCastException("The TItem and TEventItem must be the same Type."); - } - - return items.AsEnumerable(); - }); - } - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, Func, CancellationToken, Task>>> beforeEvents, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultAsync(request, null, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, string? eventName, Func, CancellationToken, Task>>> beforeEvents, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultInternalAsync(request, false, default!, eventName, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// The The value (already deserialized). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, TColl value, Func, CancellationToken, Task>>> beforeEvents, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultAsync(request, value, null, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - /// The collection - /// The collection item . - /// The item (where different to the request). - /// The . - /// The The value (already deserialized). - /// The optional event destintion name (e.g. Queue or Topic name). - /// A function that enables the collection to be processed/validated before the underlying event publishing logic is enacted. - /// An action to enable each item instance to be modified prior to publish. - /// Overrides the default (see ) maximum publish collection size. - /// The where successful. - /// The . - /// The to validate the deserialized value. - /// The . - /// The corresponding where successful. - /// The must have an of . - public Task PublishCollectionWithResultAsync(HttpRequest request, TColl value, string? eventName, Func, CancellationToken, Task>>> beforeEvents, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - => PublishCollectionWithResultInternalAsync(request, true, value, eventName, beforeEvents, eventModifier, maxCollSize, statusCode, operationType, validator, cancellationToken); - - /// - /// Performs a operation with a request JSON content value of where each item is to be published using the (with a ). - /// - private async Task PublishCollectionWithResultInternalAsync(HttpRequest request, bool useValue, TColl value, string? eventName, Func, CancellationToken, Task>>>? beforeEvents = null, Action? eventModifier = null, int? maxCollSize = null, - HttpStatusCode statusCode = HttpStatusCode.Accepted, OperationType operationType = OperationType.Unspecified, IValidator? validator = null, CancellationToken cancellationToken = default) where TColl : IEnumerable - { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (beforeEvents is null) - throw new ArgumentNullException(nameof(beforeEvents)); - - if (request.Method != HttpMethods.Post) - throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PublishWithResultAsync)}.", nameof(request)); - - return await RunAsync(request, async (wap, ct) => - { - var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, cancellationToken).ConfigureAwait(false); - if (vex != null) - return await CreateActionResultFromExceptionAsync(this, request.HttpContext, vex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - - var max = maxCollSize ?? Settings.MaxPublishCollSize; - var count = wapv!.Value?.Count() ?? 0; - if (count > max) - { - Logger.LogWarning("The publish collection contains {EventsCount} items where only a maximum size of {MaxCollSize} is supported; request has been rejected.", count, max); - var bex = new BusinessException($"The publish collection contains {count} items where only a maximum size of {max} is supported."); - return await CreateActionResultFromExceptionAsync(this, request.HttpContext, bex, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - } - - if (count == 0) - return new AcceptedResult(); + // Process the publishing as configured. + var r = await Result.Go() + .WhenAsAsync(() => args.OnBeforeValidationAsync is not null, () => args.OnBeforeValidationAsync!(wapc!, ct)) + .WhenAsAsync(_ => args.Validator is not null, async _ => (await args.Validator!.ValidateAsync(wapc!.Value!, ct).ConfigureAwait(false)).ToResult()) + .WhenAsync(_ => args.OnBeforeEventAsync is not null, _ => args.OnBeforeEventAsync!(wapc!, ct)); - var result = await beforeEvents(wapv!, ct).ConfigureAwait(false); - if (result.IsFailure) - return await CreateActionResultFromExceptionAsync(this, request.HttpContext, result.Error, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); + if (r.IsFailure) + return await CreateActionResultFromExceptionAsync(this, request.HttpContext, r.Error, Settings, Logger, OnUnhandledExceptionAsync, cancellationToken).ConfigureAwait(false); - foreach (var item in result.Value) + // Create the events (also performing mapping where applicable). + foreach (var item in wapc!.Value!) { - var @event = new EventData { Value = item }; - eventModifier?.Invoke(@event); + var @event = new EventData { Value = args.AreSameType ? item : (args.Mapper is not null ? args.Mapper.Map(item) : Mapper.Map(item)) }; + args.OnEvent?.Invoke(@event); - if (eventName == null) + if (args.EventName is null) EventPublisher.Publish(@event); else - EventPublisher.PublishNamed(eventName, @event); + EventPublisher.PublishNamed(args.EventName, @event); } await EventPublisher.SendAsync(ct).ConfigureAwait(false); - return new ExtendedStatusCodeResult(statusCode); - }, operationType, cancellationToken, nameof(PublishWithResultAsync)).ConfigureAwait(false); + return args.CreateSuccessResult?.Invoke() ?? new ExtendedStatusCodeResult(args.StatusCode); + }, args.OperationType, cancellationToken, nameof(PublishAsync)).ConfigureAwait(false); } #endregion diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT.cs b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT.cs new file mode 100644 index 00000000..9d3778e6 --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT.cs @@ -0,0 +1,57 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Represents the arguments; being the opportunity to further configure the standard processing. + /// + /// The request JSON content value . + /// The optional validator. + public class WebApiPublisherArgs(IValidator? validator = null) : IWebApiPublisherArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The event destination name (e.g. Queue or Topic name). + /// The optional validator. + public WebApiPublisherArgs(string eventName, IValidator? validator = null) : this(validator) => EventName = eventName; + + /// + public string? EventName { get; set; } + + /// + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Accepted; + + /// + public IValidator? Validator { get; set; } = validator; + + /// + public OperationType OperationType { get; set; } = OperationType.Unspecified; + + /// + public Func, CancellationToken, Task>? OnBeforeValidationAsync { get; set; } + + /// + public Func, CancellationToken, Task>? OnBeforeEventAsync { get; set; } + + /// + public Action? OnEvent { get; set; } + + /// + IMapper? IWebApiPublisherArgs.Mapper => null; + + /// + public Func? CreateSuccessResult { get; set; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT2.cs b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT2.cs new file mode 100644 index 00000000..bb03a195 --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherArgsT2.cs @@ -0,0 +1,57 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Represents the arguments; being the opportunity to further configure the standard processing. + /// + /// The request JSON content value . + /// (where different to the request). + /// The optional validator. + public class WebApiPublisherArgs(IValidator? validator = null) : IWebApiPublisherArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The event destination name (e.g. Queue or Topic name). + /// The optional validator. + public WebApiPublisherArgs(string eventName, IValidator? validator = null) : this(validator) => EventName = eventName; + + /// + public string? EventName { get; set; } = default!; + + /// + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Accepted; + + /// + public IValidator? Validator { get; set; } = validator; + + /// + public OperationType OperationType { get; set; } = OperationType.Unspecified; + + /// + public Func, CancellationToken, Task>? OnBeforeValidationAsync { get; set; } + + /// + public Func, CancellationToken, Task>? OnBeforeEventAsync { get; set; } + + /// + public Action? OnEvent { get; set; } + + /// + public IMapper? Mapper { get; set; } + + /// + public Func? CreateSuccessResult { get; set; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT.cs b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT.cs new file mode 100644 index 00000000..8a8b9f2a --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT.cs @@ -0,0 +1,61 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Represents the collection-based arguments; being the opportunity to further configure the standard processing. + /// + /// The request JSON collection . + /// The collection item . + /// The optional validator. + public class WebApiPublisherCollectionArgs(IValidator? validator = null) : IWebApiPublisherCollectionArgs where TColl : IEnumerable + { + /// + /// Initializes a new instance of the class. + /// + /// The event destination name (e.g. Queue or Topic name). + /// The optional validator. + public WebApiPublisherCollectionArgs(string eventName, IValidator? validator = null) : this(validator) => EventName = eventName; + + /// + public string? EventName { get; set; } + + /// + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Accepted; + + /// + public int? MaxCollectionSize { get; set; } + + /// + public IValidator? Validator { get; set; } = validator; + + /// + public OperationType OperationType { get; set; } = OperationType.Unspecified; + + /// + public Func, CancellationToken, Task>? OnBeforeValidationAsync { get; set; } + + /// + public Func, CancellationToken, Task>? OnBeforeEventAsync { get; set; } + + /// + public Action? OnEvent { get; set; } + + /// + IMapper? IWebApiPublisherCollectionArgs.Mapper => null; + + /// + public Func? CreateSuccessResult { get; set; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT2.cs b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT2.cs new file mode 100644 index 00000000..573d1aa7 --- /dev/null +++ b/src/CoreEx.AspNetCore/WebApis/WebApiPublisherCollectionArgsT2.cs @@ -0,0 +1,62 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using CoreEx.Events; +using CoreEx.Mapping; +using CoreEx.Results; +using CoreEx.Validation; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace CoreEx.AspNetCore.WebApis +{ + /// + /// Represents the collection-based arguments; being the opportunity to further configure the standard processing. + /// + /// The request JSON collection . + /// The collection item . + /// The -equivalent (where different then a will be required). + /// The optional validator. + public class WebApiPublisherCollectionArgs(IValidator? validator = null) : IWebApiPublisherCollectionArgs where TColl : IEnumerable + { + /// + /// Initializes a new instance of the class. + /// + /// The event destination name (e.g. Queue or Topic name). + /// The optional validator. + public WebApiPublisherCollectionArgs(string eventName, IValidator? validator = null) : this(validator) => EventName = eventName; + + /// + public string? EventName { get; set; } + + /// + public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Accepted; + + /// + public int? MaxCollectionSize { get; set; } + + /// + public IValidator? Validator { get; set; } = validator; + + /// + public OperationType OperationType { get; set; } = OperationType.Unspecified; + + /// + public Func, CancellationToken, Task>? OnBeforeValidationAsync { get; set; } + + /// + public Func, CancellationToken, Task>? OnBeforeEventAsync { get; set; } + + /// + public Action? OnEvent { get; set; } + + /// + public IMapper? Mapper { get; set; } + + /// + public Func? CreateSuccessResult { get; set; } + } +} \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiRequestOptions.cs b/src/CoreEx.AspNetCore/WebApis/WebApiRequestOptions.cs index e98820dd..7c6cc81a 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiRequestOptions.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiRequestOptions.cs @@ -24,7 +24,7 @@ public class WebApiRequestOptions /// The . public WebApiRequestOptions(HttpRequest httpRequest) { - Request = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest)); + Request = httpRequest.ThrowIfNull(nameof(httpRequest)); GetQueryStringOptions(Request.Query); if (httpRequest.Headers != null && httpRequest.Headers.Count > 0) diff --git a/src/CoreEx.AspNetCore/WebApis/WebApiWithResult.cs b/src/CoreEx.AspNetCore/WebApis/WebApiWithResult.cs index a96da6cd..1e6d80e3 100644 --- a/src/CoreEx.AspNetCore/WebApis/WebApiWithResult.cs +++ b/src/CoreEx.AspNetCore/WebApis/WebApiWithResult.cs @@ -46,15 +46,12 @@ public Task GetWithResultAsync(HttpRequest request, Func public async Task GetWithResultAsync(HttpRequest request, Func>> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NotFound, OperationType operationType = OperationType.Read, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsGet(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Get}' to use {nameof(GetAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -92,15 +89,12 @@ public Task PostWithResultAsync(HttpRequest request, Func PostWithResultAsync(HttpRequest request, Func> function, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Create, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -184,15 +178,12 @@ public Task PostWithResultAsync(HttpRequest request, TVal private async Task PostWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Create, bool valueIsRequired = true, IValidator? validator = null, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -236,15 +227,12 @@ public Task PostWithResultAsync(HttpRequest request, Fun public async Task PostWithResultAsync(HttpRequest request, Func>> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Create, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -336,15 +324,12 @@ public Task PostWithResultAsync(HttpRequest requ private async Task PostWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task>> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Create, bool valueIsRequired = true, IValidator? validator = null, Func? locationUri = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPost(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Post}' to use {nameof(PostAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, ct).ConfigureAwait(false); @@ -432,15 +417,12 @@ public Task PutWithResultAsync(HttpRequest request, TValu private async Task PutWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task> function, HttpStatusCode statusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Update, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutWithResultAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, cancellationToken).ConfigureAwait(false); @@ -532,15 +514,12 @@ public Task PutWithResultAsync(HttpRequest reque public async Task PutWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, Func, CancellationToken, Task>> function, HttpStatusCode statusCode = HttpStatusCode.OK, HttpStatusCode alternateStatusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Update, bool valueIsRequired = true, IValidator? validator = null, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutWithResultAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, valueIsRequired, validator, ct).ConfigureAwait(false); @@ -628,18 +607,13 @@ public Task PutWithResultAsync(HttpRequest request, TValu private async Task PutWithResultInternalAsync(HttpRequest request, bool useValue, TValue value, Func>> get, Func, CancellationToken, Task>> put, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Update, IValidator? validator = null, bool simulatedConcurrency = false, CancellationToken cancellationToken = default) where TValue : class { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + get.ThrowIfNull(nameof(get)); + put.ThrowIfNull(nameof(put)); if (!HttpMethods.IsPut(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Put}' to use {nameof(PutWithResultAsync)}.", nameof(request)); - if (get == null) - throw new ArgumentNullException(nameof(get)); - - if (put == null) - throw new ArgumentNullException(nameof(put)); - return await RunAsync(request, async (wap, ct) => { var (wapv, vex) = await ValidateValueAsync(wap, useValue, value, true, validator, ct).ConfigureAwait(false); @@ -687,15 +661,12 @@ public Task DeleteWithResultAsync(HttpRequest request, FuncThe corresponding where successful. public async Task DeleteWithResultAsync(HttpRequest request, Func> function, HttpStatusCode statusCode = HttpStatusCode.NoContent, OperationType operationType = OperationType.Delete, CancellationToken cancellationToken = default) { - if (request == null) - throw new ArgumentNullException(nameof(request)); + request.ThrowIfNull(nameof(request)); + function.ThrowIfNull(nameof(function)); if (!HttpMethods.IsDelete(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Delete}' to use {nameof(DeleteWithResultAsync)}.", nameof(request)); - if (function == null) - throw new ArgumentNullException(nameof(function)); - return await RunAsync(request, async (wap, ct) => { var result = await function(wap, ct).ConfigureAwait(false); @@ -748,21 +719,16 @@ public Task PatchWithResultAsync(HttpRequest request, Fun public async Task PatchWithResultAsync(HttpRequest request, Func>> get, Func, CancellationToken, Task>> put, HttpStatusCode statusCode = HttpStatusCode.OK, OperationType operationType = OperationType.Update, IValidator? validator = null, bool simulatedConcurrency = false, CancellationToken cancellationToken = default) where TValue : class { + request.ThrowIfNull(nameof(request)); + get.ThrowIfNull(nameof(get)); + put.ThrowIfNull(nameof(put)); + if (JsonMergePatch == null) throw new InvalidOperationException($"To use the '{nameof(PatchWithResultAsync)}' methods the '{nameof(JsonMergePatch)}' object must be passed in the constructor. Where using dependency injection consider using '{nameof(Microsoft.Extensions.DependencyInjection.IServiceCollectionExtensions.AddJsonMergePatch)}' to add and configure the supported options."); - if (request == null) - throw new ArgumentNullException(nameof(request)); - if (!HttpMethods.IsPatch(request.Method)) throw new ArgumentException($"HttpRequest.Method is '{request.Method}'; must be '{HttpMethods.Patch}' to use {nameof(PatchWithResultAsync)}.", nameof(request)); - if (get == null) - throw new ArgumentNullException(nameof(get)); - - if (put == null) - throw new ArgumentNullException(nameof(put)); - return await RunAsync(request, async (wap, ct) => { // Make sure that the only the support content types are used. diff --git a/src/CoreEx.Azure/ServiceBus/README.md b/src/CoreEx.Azure/ServiceBus/README.md index 78eca385..f8a0bf0c 100644 --- a/src/CoreEx.Azure/ServiceBus/README.md +++ b/src/CoreEx.Azure/ServiceBus/README.md @@ -46,6 +46,8 @@ public class ServiceBusExecuteVerificationFunction } ``` +
+ ### Instrumentation To get further insights into the processing of the messages an [`IEventSubscriberInstrumentation`](../../CoreEx/Events/IEventSubscriberInstrumentation.cs) can be implemented. The corresponding `EventSubscriberBase.Instrumentation` property should be set during construction; typically performed during dependency injection. Determine whether the instrumentation instance should also be registered as a _singleton_. diff --git a/src/CoreEx.Validation/ValidationArgs.cs b/src/CoreEx.Validation/ValidationArgs.cs index 09687ba6..e92ceabb 100644 --- a/src/CoreEx.Validation/ValidationArgs.cs +++ b/src/CoreEx.Validation/ValidationArgs.cs @@ -1,5 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx +using CoreEx.Configuration; using CoreEx.Entities; using System.Collections.Generic; @@ -10,6 +11,8 @@ namespace CoreEx.Validation ///
public class ValidationArgs { + private static bool? _defaultUseJsonNames; + /// /// Initializes a new instance of the class. /// @@ -18,7 +21,12 @@ public ValidationArgs() { } /// /// Indicates whether to use the JSON name for the ; by default (false) uses the .NET name. /// - public static bool DefaultUseJsonNames { get; set; } = false; + /// Will attempt to use as a default where possible. + public static bool DefaultUseJsonNames + { + get => _defaultUseJsonNames ?? ExecutionContext.GetService()?.ValidationUseJsonNames ?? false; + set => _defaultUseJsonNames = value; + } /// /// Gets or sets the optional name of a selected (specific) property to validate for the entity (null indicates to validate all). diff --git a/src/CoreEx.Validation/ValidationExtensions.cs b/src/CoreEx.Validation/ValidationExtensions.cs index 87395573..ef47a01a 100644 --- a/src/CoreEx.Validation/ValidationExtensions.cs +++ b/src/CoreEx.Validation/ValidationExtensions.cs @@ -15,9 +15,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using static System.Net.Mime.MediaTypeNames; -using System.Xml.Linq; -using System.Data; namespace CoreEx.Validation { diff --git a/src/CoreEx/Configuration/SettingsBase.cs b/src/CoreEx/Configuration/SettingsBase.cs index 5a814063..dd1e034d 100644 --- a/src/CoreEx/Configuration/SettingsBase.cs +++ b/src/CoreEx/Configuration/SettingsBase.cs @@ -21,6 +21,7 @@ public abstract class SettingsBase private readonly ThreadLocal _isReflectionCall = new(); private readonly List _prefixes = []; private readonly Dictionary _allProperties; + private bool? _validationUseJsonNames; /// /// Initializes a new instance of the class. @@ -205,5 +206,10 @@ public T GetRequiredValue([CallerMemberName] string key = "") /// Gets the default . Defaults to 30 minutes. /// public TimeSpan? RefDataCacheSlidingExpiration => GetValue($"RefDataCache__{nameof(ICacheEntry.SlidingExpiration)}", TimeSpan.FromMinutes(30)); + + /// + /// Gets the default validation use of JSON names. Defaults to true. + /// + public bool ValidationUseJsonNames => _validationUseJsonNames ??= GetValue(nameof(ValidationUseJsonNames), true); } } \ No newline at end of file diff --git a/src/CoreEx/Entities/PagingArgs.cs b/src/CoreEx/Entities/PagingArgs.cs index 5ca5002e..fe510b69 100644 --- a/src/CoreEx/Entities/PagingArgs.cs +++ b/src/CoreEx/Entities/PagingArgs.cs @@ -10,7 +10,7 @@ namespace CoreEx.Entities /// Represents position-based paging being a) and , b) and , or c) and . The and (and ) /// are static settings to encourage page-size consistency, as well as limit the maximum value possible. /// - //[System.Diagnostics.DebuggerStepThrough] + [System.Diagnostics.DebuggerStepThrough] public class PagingArgs : IEquatable { private static long? _defaultTake; diff --git a/src/CoreEx/Hosting/README.md b/src/CoreEx/Hosting/README.md index 5cee570d..8862ec52 100644 --- a/src/CoreEx/Hosting/README.md +++ b/src/CoreEx/Hosting/README.md @@ -1,18 +1,54 @@ # CoreEx.Hosting -The `CoreEx.Hosting` namespace provides additional [hosted service (worker)](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers) capabilities. +The `CoreEx.Hosting` namespace provides additional [hosted service (worker)](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers) runtime capabilities.
## Motivation -To enable additional [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) capabilities. +To enable improved hosted service consistency and testability, plus additional [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) runtime capabilities. + +
+ +## Host startup + +To improve consistency and testability the [`IHostStartup`](./IHostStartup.cs) and [`HostStartup`](./HostStartup) implementations are provided. By seperating out the key [Dependency Injection (DI)](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) configuration from the underlying host configuration enables the DI configuration to be tested in isolation against a _test-host_ where applicable. + +The following is an example of a `HostStartup` implementation. + +```csharp +public class Startup : HostStartup +{ + public override void ConfigureAppConfiguration(HostBuilderContext context, IConfigurationBuilder config) + { + config.AddEnvironmentVariables("Prefix_"); + } + + /// + public override void ConfigureServices(IServiceCollection services) + { + services + .AddSettings() + .AddExecutionContext() + .AddJsonSerializer(); + } +} +``` + +The following is an example of a `Program` implementation that initiates a host and uses the [`ConfigureHostStartup`](HostStartupExtensions.cs) extension method to integrate the `Startup` functionality. This has an added advantage of being able to add specific startup capabilities directly to a host that should not be available to the _test-host_ (as demonstrated by `ConfigureFunctionsWorkerDefaults`). + +```csharp +new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureHostStartup() + .Build().Run(); +```
## Hosted services -The following additional `IHostedService` implementations are provided. +The following additional [`IHostedService`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) implementations are provided. Class | Description -|- @@ -28,4 +64,4 @@ To ensure only a single host can perform _work_ at a time concurrency implementa Class | Description -|- [`ConcurrentSynchronizer`](./ConcurrentSynchronizer.cs) | Performs _no_ synchronization in that `Enter` will always return `true` resulting in concurrent execution. -[`FileLockSynchronizer`](./FileLockSynchronizer.cs) | Performs synchronization by taking an exclusive lock on a file. +[`FileLockSynchronizer`](./FileLockSynchronizer.cs) | Performs synchronization by taking an exclusive lock on a file. \ No newline at end of file diff --git a/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs b/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs index 6373c35e..227656cf 100644 --- a/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs +++ b/tests/CoreEx.Test/Framework/WebApis/WebApiPublisherTest.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using System.Net.Http; using System.Threading.Tasks; -using UnitTestEx; using UnitTestEx.NUnit; using CoreEx.Mapping; using Microsoft.Extensions.DependencyInjection; @@ -23,7 +22,7 @@ public void PublishAsync_Value_Success() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "A", Name = "B", Price = 1.99m }), "test")) + .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "A", Name = "B", Price = 1.99m }), new WebApiPublisherArgs("test"))) .ToActionResultAssertor() .AssertAccepted(); @@ -43,7 +42,7 @@ public void PublishAsync_Value_Error() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishAsync(test.CreateHttpRequest(HttpMethod.Post, "https://unittest"), "test")) + .Run(f => f.PublishAsync(test.CreateHttpRequest(HttpMethod.Post, "https://unittest"), new WebApiPublisherArgs("test"))) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("Invalid request: content was not provided, contained invalid JSON, or was incorrectly formatted: Value is mandatory."); @@ -60,7 +59,7 @@ public void PublishAsync_Value_Mapper() test.ReplaceScoped(_ => imp) .ConfigureServices(sc => sc.AddMappers()) .Type() - .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "A", Name = "B", Price = 1.99m }), "test")) + .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "A", Name = "B", Price = 1.99m }), new WebApiPublisherArgs("test"))) .ToActionResultAssertor() .AssertAccepted(); @@ -87,7 +86,7 @@ public void PublishCollectionAsync_Success() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test")) + .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), new WebApiPublisherCollectionArgs("test"))) .ToActionResultAssertor() .AssertAccepted(); @@ -117,7 +116,7 @@ public void PublishCollectionAsync_Success_Mapper() test.ReplaceScoped(_ => imp) .ConfigureServices(sc => sc.AddMappers()) .Type() - .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test")) + .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), new WebApiPublisherCollectionArgs("test"))) .ToActionResultAssertor() .AssertAccepted(); @@ -149,7 +148,7 @@ public void PublishCollectionAsync_Success_WithCorrelationId() test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionAsync(hr, "test")) + .Run(f => f.PublishCollectionAsync(hr, new WebApiPublisherCollectionArgs("test"))) .ToActionResultAssertor() .AssertAccepted(); @@ -186,7 +185,7 @@ public void PublishCollectionAsync_SizeError() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test", maxCollSize: 2)) + .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), new WebApiPublisherCollectionArgs("test") { MaxCollectionSize = 2 })) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("The publish collection contains 3 items where only a maximum size of 2 is supported."); @@ -202,7 +201,8 @@ public void PublishAsync_BeforeError() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }), "test", beforeEvent: (_, __) => throw new BusinessException("Nope, nope!"))) + .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }), + new WebApiPublisherArgs { EventName = "test", OnBeforeEventAsync = (_, __) => throw new BusinessException("Nope, nope!") })) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("Nope, nope!"); @@ -212,20 +212,14 @@ public void PublishAsync_BeforeError() } [Test] - public void PublishCollectionAsync_BeforeError() + public void PublishAsync_BeforeErrorResult() { - var products = new ProductCollection - { - new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }, - new Product { Id = "Xyz2", Name = "Widget2", Price = 9.95m }, - new Product { Id = "Xyz3", Name = "Widget3", Price = 9.95m } - }; - var imp = new InMemoryPublisher(); using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test", beforeEvents: (_, __) => throw new BusinessException("Nope, nope!"))) + .Run(f => f.PublishAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }), + new WebApiPublisherArgs { EventName = "test", OnBeforeEventAsync = (_, __) => Task.FromResult(Result.Fail("Nope, nope!")) })) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("Nope, nope!"); @@ -235,112 +229,7 @@ public void PublishCollectionAsync_BeforeError() } [Test] - public void PublishWithResultAsync_Value_Success() - { - var imp = new InMemoryPublisher(); - using var test = FunctionTester.Create(); - test.ReplaceScoped(_ => imp) - .Type() - .Run(f => f.PublishWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "A", Name = "B", Price = 1.99m }), "test")) - .ToActionResultAssertor() - .AssertAccepted(); - - var qn = imp.GetNames(); - Assert.That(qn, Has.Length.EqualTo(1)); - Assert.That(qn[0], Is.EqualTo("test")); - - var ed = imp.GetEvents("test"); - Assert.That(ed, Has.Length.EqualTo(1)); - ObjectComparer.Assert(new Product { Id = "A", Name = "B", Price = 1.99m }, ed[0].Value); - } - - [Test] - public void PublishWithResultAsync_Value_Error() - { - var imp = new InMemoryPublisher(); - using var test = FunctionTester.Create(); - test.ReplaceScoped(_ => imp) - .Type() - .Run(f => f.PublishWithResultAsync(test.CreateHttpRequest(HttpMethod.Post, "https://unittest"), "test")) - .ToActionResultAssertor() - .AssertBadRequest() - .AssertContent("Invalid request: content was not provided, contained invalid JSON, or was incorrectly formatted: Value is mandatory."); - - var qn = imp.GetNames(); - Assert.That(qn, Is.Empty); - } - - [Test] - public void PublishCollectionWithResultAsync_Success() - { - var products = new ProductCollection - { - new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }, - new Product { Id = "Xyz2", Name = "Widget2", Price = 9.95m }, - new Product { Id = "Xyz3", Name = "Widget3", Price = 9.95m } - }; - - var imp = new InMemoryPublisher(); - using var test = FunctionTester.Create(); - test.ReplaceScoped(_ => imp) - .Type() - .Run(f => f.PublishCollectionWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test")) - .ToActionResultAssertor() - .AssertAccepted(); - - var qn = imp.GetNames(); - Assert.That(qn, Has.Length.EqualTo(1)); - Assert.That(qn[0], Is.EqualTo("test")); - - var ed = imp.GetEvents("test"); - Assert.That(ed, Has.Length.EqualTo(3)); - ObjectComparer.Assert(products[0], ed[0].Value); - ObjectComparer.Assert(products[1], ed[1].Value); - ObjectComparer.Assert(products[2], ed[2].Value); - } - - [Test] - public void PublishCollectionWithResultAsync_Success_WithCorrelationId() - { - var products = new ProductCollection - { - new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }, - new Product { Id = "Xyz2", Name = "Widget2", Price = 9.95m }, - new Product { Id = "Xyz3", Name = "Widget3", Price = 9.95m } - }; - - using var test = FunctionTester.Create(); - var imp = new InMemoryPublisher(); - var hr = test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products); - hr.Headers.Add("x-correlation-id", "corr-id"); // Send through a known correlation id. - - test.ReplaceScoped(_ => imp) - .Type() - .Run(f => f.PublishCollectionWithResultAsync(hr, "test")) - .ToActionResultAssertor() - .AssertAccepted(); - - var qn = imp.GetNames(); - Assert.That(qn, Has.Length.EqualTo(1)); - Assert.That(qn[0], Is.EqualTo("test")); - - var ed = imp.GetEvents("test"); - Assert.That(ed, Has.Length.EqualTo(3)); - ObjectComparer.Assert(products[0], ed[0].Value); - ObjectComparer.Assert(products[1], ed[1].Value); - ObjectComparer.Assert(products[2], ed[2].Value); - - Assert.Multiple(() => - { - // Assert the known correlation id. - Assert.That(ed[0].CorrelationId, Is.EqualTo("corr-id")); - Assert.That(ed[1].CorrelationId, Is.EqualTo("corr-id")); - Assert.That(ed[2].CorrelationId, Is.EqualTo("corr-id")); - }); - } - - [Test] - public void PublishCollectionWithResultAsync_SizeError() + public void PublishCollectionAsync_BeforeError() { var products = new ProductCollection { @@ -353,23 +242,7 @@ public void PublishCollectionWithResultAsync_SizeError() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test", maxCollSize: 2)) - .ToActionResultAssertor() - .AssertBadRequest() - .AssertContent("The publish collection contains 3 items where only a maximum size of 2 is supported."); - - var qn = imp.GetNames(); - Assert.That(qn, Is.Empty); - } - - [Test] - public void PublishWithResultAsync_BeforeError() - { - var imp = new InMemoryPublisher(); - using var test = FunctionTester.Create(); - test.ReplaceScoped(_ => imp) - .Type() - .Run(f => f.PublishWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", new Product { Id = "Xyz", Name = "Widget", Price = 9.95m }), "test", beforeEvent: (_, __) => Task.FromResult(Result.Fail("Nope, nope!")))) + .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), new WebApiPublisherCollectionArgs("test") { OnBeforeEventAsync = (_, __) => throw new BusinessException("Nope, nope!") })) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("Nope, nope!"); @@ -379,7 +252,7 @@ public void PublishWithResultAsync_BeforeError() } [Test] - public void PublishCollectionWithResultAsync_BeforeError() + public void PublishCollectionAsync_BeforeErrorResult() { var products = new ProductCollection { @@ -392,7 +265,7 @@ public void PublishCollectionWithResultAsync_BeforeError() using var test = FunctionTester.Create(); test.ReplaceScoped(_ => imp) .Type() - .Run(f => f.PublishCollectionWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), "test", beforeEvents: (_, __) => Task.FromResult(Result.Fail("Nope, nope!")))) + .Run(f => f.PublishCollectionAsync(test.CreateJsonHttpRequest(HttpMethod.Post, "https://unittest", products), new WebApiPublisherCollectionArgs("test") { OnBeforeEventAsync = (_, __) => Task.FromResult(Result.Fail("Nope, nope!")) })) .ToActionResultAssertor() .AssertBadRequest() .AssertContent("Nope, nope!"); diff --git a/tests/CoreEx.TestFunction/Functions/HttpTriggerPublishFunction.cs b/tests/CoreEx.TestFunction/Functions/HttpTriggerPublishFunction.cs index 576129f3..3652d3bc 100644 --- a/tests/CoreEx.TestFunction/Functions/HttpTriggerPublishFunction.cs +++ b/tests/CoreEx.TestFunction/Functions/HttpTriggerPublishFunction.cs @@ -21,6 +21,6 @@ public HttpTriggerPublishFunction(WebApiPublisher webApiPublisher) [FunctionName("HttpTriggerPublishFunction")] public Task RunAsync([HttpTrigger(AuthorizationLevel.Function, "post", Route = "products/publish")] HttpRequest request) - => _webApiPublisher.PublishAsync(request, "test-queue", validator: new ProductValidator().Wrap()); + => _webApiPublisher.PublishAsync(request, new WebApiPublisherArgs("test-queue", new ProductValidator().Wrap())); } } \ No newline at end of file