Skip to content

Commit

Permalink
IActionResult as-is and PagingArgs.Token. (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
chullybun authored Jan 6, 2024
1 parent 4b6b075 commit 5eeb7ac
Show file tree
Hide file tree
Showing 54 changed files with 429 additions and 171 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## v3.8.0
- *Enhancement*: The `ValueContentResult.CreateResult` has been updated to return the resulting value as-is where is an instance of `IActionResult`; otherwise, converts `value` to a `ValueContentResult` (previous behavior).
- *Enhancement*: The `PagingArgs` has been extended to support `Token`; being a continuation token to enable paging to be performed where the underlying data source does not support skip/take-style paging.

## v3.7.2
- *Fixed*: The `ReferenceDataMultiCollection` and `ReferenceDataMultiItem` have been replaced with the `ReferenceDataMultiDictionary` as existing resulted in an unintended format with which to return the data. This fix also removed the need for the `ReferenceDataMultiCollectionConverterFactory` as custom serialization for this is no longer required.

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>3.7.2</Version>
<Version>3.8.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
2 changes: 1 addition & 1 deletion samples/My.Hr/My.Hr.Api/My.Hr.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.0.0-beta.5" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions samples/My.Hr/My.Hr.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public void ConfigureServices(IServiceCollection services)
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING")))
{
services.AddOpenTelemetry().UseAzureMonitor();
services.Configure<AspNetCoreInstrumentationOptions>(options => options.RecordException = true);
//services.Configure<AspNetCoreInstrumentationOptions>(options => options.RecordException = true);
services.ConfigureOpenTelemetryTracerProvider((sp, builder) => builder.AddSource("CoreEx.*"));
}

Expand All @@ -73,7 +73,7 @@ public void ConfigureServices(IServiceCollection services)
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
options.OperationFilter<AcceptsBodyOperationFilter>(); // Needed to support AcceptsBodyAttribue where body parameter not explicitly defined.
options.OperationFilter<PagingOperationFilter>(PagingOperationFilterFields.PageSizeCount); // Needed to support PagingAttribue where PagingArgs parameter not explicitly defined.
options.OperationFilter<PagingOperationFilter>(PagingOperationFilterFields.TokenTake); // Needed to support PagingAttribue where PagingArgs parameter not explicitly defined.
});
}

Expand Down
2 changes: 1 addition & 1 deletion samples/My.Hr/My.Hr.Database/My.Hr.Database.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="DbEx.SqlServer" Version="2.3.12" />
<PackageReference Include="DbEx.SqlServer" Version="2.3.14" />
<PackageReference Include="Microsoft.Tye.Extensions.Configuration" Version="0.10.0-alpha.21420.1" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion samples/My.Hr/My.Hr.Functions/My.Hr.Functions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.OpenApi" Version="1.5.1" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.3" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/My.Hr/My.Hr.UnitTest/My.Hr.UnitTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
Expand Down
23 changes: 15 additions & 8 deletions src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,22 @@ public static void AddPagingResult(this IHeaderDictionary headers, PagingResult?
if (paging == null)
return;

if (paging.IsSkipTake)
switch (paging.Option)
{
headers[HttpConsts.PagingSkipHeaderName] = paging.Skip.ToString(CultureInfo.InvariantCulture);
headers[HttpConsts.PagingTakeHeaderName] = paging.Take.ToString(CultureInfo.InvariantCulture);
}
else
{
headers[HttpConsts.PagingPageNumberHeaderName] = paging.Page!.Value.ToString(CultureInfo.InvariantCulture);
headers[HttpConsts.PagingPageSizeHeaderName] = paging.Take.ToString(CultureInfo.InvariantCulture);
case PagingOption.SkipAndTake:
headers[HttpConsts.PagingSkipHeaderName] = paging.Skip!.Value.ToString(CultureInfo.InvariantCulture);
headers[HttpConsts.PagingTakeHeaderName] = paging.Take.ToString(CultureInfo.InvariantCulture);
break;

case PagingOption.PageAndSize:
headers[HttpConsts.PagingPageNumberHeaderName] = paging.Page!.Value.ToString(CultureInfo.InvariantCulture);
headers[HttpConsts.PagingPageSizeHeaderName] = paging.Take.ToString(CultureInfo.InvariantCulture);
break;

default:
headers[HttpConsts.PagingTokenHeaderName] = paging.Token;
headers[HttpConsts.PagingTakeHeaderName] = paging.Take.ToString(CultureInfo.InvariantCulture);
break;
}

if (paging.TotalCount.HasValue)
Expand Down
9 changes: 6 additions & 3 deletions src/CoreEx.AspNetCore/WebApis/PagingOperationFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
if (Fields.HasFlag(PagingOperationFilterFields.Skip))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsSkipQueryStringName, "The specified number of elements in a sequence to bypass.", "number"));

if (Fields.HasFlag(PagingOperationFilterFields.Take))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsTakeQueryStringName, "The specified number of contiguous elements from the start of a sequence.", "number"));

if (Fields.HasFlag(PagingOperationFilterFields.Page))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsPageQueryStringName, "The page number for the elements in a sequence to select.", "number"));

if (Fields.HasFlag(PagingOperationFilterFields.Token))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsTokenQueryStringName, "The token to get the next page of elements.", "string"));

if (Fields.HasFlag(PagingOperationFilterFields.Take))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsTakeQueryStringName, "The specified number of contiguous elements from the start of a sequence.", "number"));

if (Fields.HasFlag(PagingOperationFilterFields.Size))
operation.Parameters.Add(CreateParameter(HttpConsts.PagingArgsSizeQueryStringName, "The page size being the specified number of contiguous elements from the start of a sequence.", "number"));

Expand Down
17 changes: 16 additions & 1 deletion src/CoreEx.AspNetCore/WebApis/PagingOperationFilterFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public enum PagingOperationFilterFields
/// </summary>
Count = 16,

/// <summary>
/// Indicates to include <see cref="PagingArgs.Token"/> field (named <see cref="HttpConsts.PagingArgsTokenQueryStringName"/>).
/// </summary>
Token = 32,

/// <summary>
/// Indicates to include <see cref="Skip"/> and <see cref="Take"/> fields.
/// </summary>
Expand All @@ -57,9 +62,19 @@ public enum PagingOperationFilterFields
/// </summary>
PageSizeCount = Page | Size | Count,

/// <summary>
/// Indicates to include <see cref="Token"/> and <see cref="Take"/> fields.
/// </summary>
TokenTake = Token | Take,

/// <summary>
/// Indicates to include <see cref="Token"/>, <see cref="Take"/> and <see cref="Count"/> fields.
/// </summary>
TokenTakeCount = Token | Size | Count,

/// <summary>
/// Indicates to include all fields.
/// </summary>
All = Skip | Take | Page | Size | Count
All = Skip | Take | Page | Size | Count | Token
}
}
34 changes: 21 additions & 13 deletions src/CoreEx.AspNetCore/WebApis/ValueContentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using CoreEx.Abstractions;
using CoreEx.AspNetCore.Http;
using CoreEx.Entities;
using CoreEx.Http;
using CoreEx.Json;
using CoreEx.Results;
using Microsoft.AspNetCore.Http;
Expand All @@ -21,9 +20,10 @@
namespace CoreEx.AspNetCore.WebApis
{
/// <summary>
/// Represents a <see cref="ContentResult"/> with a JSON serialized value.
/// Represents an <see cref="ExtendedContentResult"/> with a JSON serialized value.
/// </summary>
/// <remarks>This contains extended functionality to manage the setting of response headers related to <see cref="ETag"/>, <see cref="PagingResult"/> and <see cref="Location"/>.</remarks>
/// <remarks>This contains extended functionality to manage the setting of response headers related to <see cref="ETag"/>, <see cref="PagingResult"/> and <see cref="Location"/>.
/// <para>The <see cref="CreateResult{T}"/> and <see cref="TryCreateValueContentResult{T}"/> will return the value as-is where it is an instance of <see cref="IActionResult"/>; i.e. will bypass all related functionality.</para></remarks>
public sealed class ValueContentResult : ExtendedContentResult
{
/// <summary>
Expand Down Expand Up @@ -75,7 +75,7 @@ public override Task ExecuteResultAsync(ActionContext context)
}

/// <summary>
/// Creates the <see cref="IActionResult"/> as either <see cref="ValueContentResult"/> or <see cref="StatusCodeResult"/> as per <see cref="TryCreateValueContentResult"/>.
/// Creates the <see cref="IActionResult"/> as either <see cref="ValueContentResult"/> or <see cref="StatusCodeResult"/> as per <see cref="TryCreateValueContentResult"/>; unless <paramref name="value"/> is an instance of <see cref="IActionResult"/> which will return as-is.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="statusCode">The primary status code where there is a value.</param>
Expand All @@ -86,10 +86,10 @@ public override Task ExecuteResultAsync(ActionContext context)
/// <param name="location">The <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders.Location"/> <see cref="Uri"/>.</param>
/// <returns>The <see cref="IActionResult"/>.</returns>
public static IActionResult CreateResult<T>(T value, HttpStatusCode statusCode, HttpStatusCode? alternateStatusCode, IJsonSerializer jsonSerializer, WebApiRequestOptions requestOptions, bool checkForNotModified, Uri? location)
=> TryCreateValueContentResult(value, statusCode, alternateStatusCode, jsonSerializer, requestOptions, checkForNotModified, location, out var vcr, out var ar) ? vcr! : ar!;
=> TryCreateValueContentResult(value, statusCode, alternateStatusCode, jsonSerializer, requestOptions, checkForNotModified, location, out var pr, out var ar) ? pr! : ar!;

/// <summary>
/// Try and create a <see cref="ValueContentResult"/>; otherwise, a <see cref="StatusCodeResult"/>.
/// Try and create an <see cref="IActionResult"/> as either <see cref="ValueContentResult"/> or <see cref="StatusCodeResult"/> as per <see cref="TryCreateValueContentResult"/>; unless <paramref name="value"/> is an instance of <see cref="IActionResult"/> which will return as-is.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="statusCode">The primary status code where there is a value.</param>
Expand All @@ -98,14 +98,22 @@ public static IActionResult CreateResult<T>(T value, HttpStatusCode statusCode,
/// <param name="requestOptions">The <see cref="WebApiRequestOptions"/>.</param>
/// <param name="checkForNotModified">Indicates whether to check for <see cref="HttpStatusCode.NotModified"/> by comparing request and response <see cref="IETag.ETag"/> values.</param>
/// <param name="location">The <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders.Location"/> <see cref="Uri"/>.</param>
/// <param name="valueContentResult">The <see cref="ValueContentResult"/> where created.</param>
/// <param name="alternateResult">The alternate result where <paramref name="valueContentResult"/> not created.</param>
/// <returns><c>true</c> indicates that the <paramref name="valueContentResult"/> was created; otherwise, <c>false</c> for <paramref name="alternateResult"/> creation.</returns>
public static bool TryCreateValueContentResult<T>(T value, HttpStatusCode statusCode, HttpStatusCode? alternateStatusCode, IJsonSerializer jsonSerializer, WebApiRequestOptions requestOptions, bool checkForNotModified, Uri? location, out ValueContentResult? valueContentResult, out StatusCodeResult? alternateResult)
/// <param name="primaryResult">The <see cref="IActionResult"/> where created.</param>
/// <param name="alternateResult">The alternate result where no <paramref name="primaryResult"/>.</param>
/// <returns><c>true</c> indicates that the <paramref name="primaryResult"/> was created; otherwise, <c>false</c> for <paramref name="alternateResult"/> creation.</returns>
public static bool TryCreateValueContentResult<T>(T value, HttpStatusCode statusCode, HttpStatusCode? alternateStatusCode, IJsonSerializer jsonSerializer, WebApiRequestOptions requestOptions, bool checkForNotModified, Uri? location, out IActionResult? primaryResult, out IActionResult? alternateResult)
{
if (value is Results.IResult)
throw new ArgumentException($"The {nameof(value)} must not implement {nameof(Results.IResult)}; the underlying {nameof(Results.IResult.Value)} must be unwrapped before invoking.", nameof(value));

// Where already an IActionResult then return as-is.
if (value is IActionResult iar)
{
primaryResult = iar;
alternateResult = null;
return true;
}

object? val;
PagingResult? paging;

Expand All @@ -126,7 +134,7 @@ public static bool TryCreateValueContentResult<T>(T value, HttpStatusCode status
{
if (alternateStatusCode.HasValue)
{
valueContentResult = null;
primaryResult = null;
alternateResult = new StatusCodeResult((int)alternateStatusCode);
return false;
}
Expand Down Expand Up @@ -163,13 +171,13 @@ public static bool TryCreateValueContentResult<T>(T value, HttpStatusCode status
// Check for not-modified and return status accordingly.
if (checkForNotModified && etag == requestOptions.ETag)
{
valueContentResult = null;
primaryResult = null;
alternateResult = new StatusCodeResult((int)HttpStatusCode.NotModified);
return false;
}

// Create and return the ValueContentResult.
valueContentResult = new ValueContentResult(json, statusCode, etag, paging, location);
primaryResult = new ValueContentResult(json, statusCode, etag, paging, location);
alternateResult = null;
return true;
}
Expand Down
22 changes: 8 additions & 14 deletions src/CoreEx.AspNetCore/WebApis/WebApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,18 @@ namespace CoreEx.AspNetCore.WebApis
/// <summary>
/// Provides the core (<see cref="HttpMethods.Get"/>, <see cref="HttpMethods.Post"/>, <see cref="HttpMethods.Put"/> and <see cref="HttpMethods.Delete"/>) Web API execution encapsulation.
/// </summary>
public partial class WebApi : WebApiBase
/// <param name="executionContext">The <see cref="ExecutionContext"/>.</param>
/// <param name="settings">The <see cref="SettingsBase"/>.</param>
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="invoker">The <see cref="WebApiInvoker"/>; defaults where not specified.</param>
/// <param name="jsonMergePatch">The <see cref="IJsonMergePatch"/> to support the <see cref="HttpMethods.Patch"/> operations.</param>
public partial class WebApi(ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger<WebApi> logger, WebApiInvoker? invoker = null, IJsonMergePatch? jsonMergePatch = null) : WebApiBase(executionContext, settings, jsonSerializer, logger, invoker)
{
/// <summary>
/// Initializes a new instance of the <see cref="WebApi"/> class.
/// </summary>
/// <param name="executionContext">The <see cref="ExecutionContext"/>.</param>
/// <param name="settings">The <see cref="SettingsBase"/>.</param>
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="invoker">The <see cref="WebApiInvoker"/>; defaults where not specified.</param>
/// <param name="jsonMergePatch">The <see cref="IJsonMergePatch"/> to support the <see cref="HttpMethods.Patch"/> operations.</param>
public WebApi(ExecutionContext executionContext, SettingsBase settings, IJsonSerializer jsonSerializer, ILogger<WebApi> logger, WebApiInvoker? invoker = null, IJsonMergePatch? jsonMergePatch = null)
: base(executionContext, settings, jsonSerializer, logger, invoker) => JsonMergePatch = jsonMergePatch;

/// <summary>
/// Gets the <see cref="IJsonMergePatch"/>.
/// </summary>
public IJsonMergePatch? JsonMergePatch { get; }
public IJsonMergePatch? JsonMergePatch { get; } = jsonMergePatch;

/// <summary>
/// Indicates whether to convert a <see cref="NotFoundException"/> to the default <see cref="HttpStatusCode"/> on delete (see <see cref="DeleteAsync(HttpRequest, Func{WebApiParam, CancellationToken, Task}, HttpStatusCode, OperationType, CancellationToken)"/>.
Expand Down
11 changes: 7 additions & 4 deletions src/CoreEx.AspNetCore/WebApis/WebApiRequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,19 @@ private void GetQueryStringOptions(IQueryCollection query)
long? skip = HttpExtensions.ParseLongValue(GetNamedQueryString(query, HttpConsts.PagingArgsSkipQueryStringNames));
long? take = HttpExtensions.ParseLongValue(GetNamedQueryString(query, HttpConsts.PagingArgsTakeQueryStringNames));
long? page = skip.HasValue ? null : HttpExtensions.ParseLongValue(GetNamedQueryString(query, HttpConsts.PagingArgsPageQueryStringNames));
string? token = GetNamedQueryString(query, HttpConsts.PagingArgsTokenQueryStringNames);
bool isGetCount = HttpExtensions.ParseBoolValue(GetNamedQueryString(query, HttpConsts.PagingArgsCountQueryStringNames));

if (skip == null && take == null && page == null && !isGetCount)
if (skip == null && take == null && page == null && string.IsNullOrEmpty(token) && !isGetCount)
return null;

PagingArgs paging;
if (skip == null && page == null)
paging = (take.HasValue) ? PagingArgs.CreateSkipAndTake(0, take) : new PagingArgs();
if (!string.IsNullOrEmpty(token))
paging = PagingArgs.CreateTokenAndTake(token, take);
else if (skip == null && page == null)
paging = take.HasValue ? PagingArgs.CreateSkipAndTake(0, take) : new PagingArgs();
else
paging = (skip.HasValue) ? PagingArgs.CreateSkipAndTake(skip.Value, take) : PagingArgs.CreatePageAndSize(page == null ? 0 : page.Value, take);
paging = skip.HasValue ? PagingArgs.CreateSkipAndTake(skip.Value, take) : PagingArgs.CreatePageAndSize(page == null ? 0 : page.Value, take);

paging.IsGetCount = isGetCount;
return paging;
Expand Down
Loading

0 comments on commit 5eeb7ac

Please sign in to comment.