Skip to content

Commit

Permalink
v3.27.3 (#129)
Browse files Browse the repository at this point in the history
* v3.27.3
- *Fixed:* The `ExecutionContext.Messages` were not being returned as intended within the `x-messages` HTTP Response header; enabled within the `ExtendedStatusCodeResult` and `ExtendedContentResult` on success only (status code `>= 200` and `<= 299`). Note these messages are JSON serialized as the underlying `MessageItemCollection` type.

* ixed: The AgentTester has been updated to return a HttpResultAssertor where the operation returns a HttpResult to enable further assertions to be made on the Result itself.

* Move files to correct namespace.

* Add additional tests for assertor.
  • Loading branch information
chullybun authored Oct 23, 2024
1 parent ce9f2dc commit 1dfcbda
Show file tree
Hide file tree
Showing 22 changed files with 313 additions and 60 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.27.3
- *Fixed:* The `ExecutionContext.Messages` were not being returned as intended within the `x-messages` HTTP Response header; enabled within the `ExtendedStatusCodeResult` and `ExtendedContentResult` on success only (status code `>= 200` and `<= 299`). Note these messages are JSON serialized as the underlying `MessageItemCollection` type.
- *Fixed:* The `AgentTester` has been updated to return a `HttpResultAssertor` where the operation returns a `HttpResult` to enable further assertions to be made on the `Result` itself.

## v3.27.2
- *Fixed:* The `IServiceCollection.AddCosmosDb` extension method was registering as a singleton; this has been corrected to register as scoped. The dependent `CosmosClient` should remain a singleton as is [best practice](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/best-practice-dotnet).

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.27.2</Version>
<Version>3.27.3</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
10 changes: 8 additions & 2 deletions samples/My.Hr/My.Hr.Business/Services/EmployeeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, HrSetti
_settings = settings;
}

public async Task<Employee?> GetEmployeeAsync(Guid id)
=> await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id);
public async Task<Employee?> GetEmployeeAsync(Guid id)
{
var emp = await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id);
if (emp is not null && emp.Birthday.HasValue && emp.Birthday.Value.Year < 2000)
CoreEx.ExecutionContext.Current.Messages.Add(MessageType.Warning, "Employee is considered old.");

return emp;
}

public Task<EmployeeCollectionResult> GetAllAsync(QueryArgs? query, PagingArgs? paging)
=> _dbContext.Employees.Where(_queryConfig, query).OrderBy(_queryConfig, query).ToCollectionResultAsync<EmployeeCollectionResult, EmployeeCollection, Employee>(paging);
Expand Down
19 changes: 17 additions & 2 deletions samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void A110_Get_Found()
{
using var test = ApiTester.Create<Startup>();

test.Controller<EmployeeController>()
var resp = test.Controller<EmployeeController>()
.Run(c => c.GetAsync(1.ToGuid()))
.AssertOK()
.AssertValue(new Employee
Expand All @@ -64,7 +64,22 @@ public void A110_Get_Found()
Birthday = new DateTime(1985, 03, 18, 0, 0, 0, DateTimeKind.Unspecified),
StartDate = new DateTime(2000, 12, 11, 0, 0, 0, DateTimeKind.Unspecified),
PhoneNo = "(425) 612 8113"
}, nameof(Employee.ETag));
}, nameof(Employee.ETag))
.Response;

// Also, validate the context header messages.
var result = HttpResult.CreateAsync(resp).GetAwaiter().GetResult();
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Messages, Is.Not.Null);
});
Assert.That(result.Messages, Has.Count.EqualTo(1));
Assert.Multiple(() =>
{
Assert.That(result.Messages[0].Type, Is.EqualTo(MessageType.Warning));
Assert.That(result.Messages[0].Text, Is.EqualTo("Employee is considered old."));
});
}

[Test]
Expand Down
16 changes: 16 additions & 0 deletions src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
Expand Down Expand Up @@ -172,5 +173,20 @@ public static void AddPagingResult(this IHeaderDictionary headers, PagingResult?
if (paging.TotalPages.HasValue)
headers[HttpConsts.PagingTotalPagesHeaderName] = paging.TotalPages.Value.ToString(CultureInfo.InvariantCulture);
}

/// <summary>
/// Adds the <see cref="MessageItemCollection"/> to the <see cref="IHeaderDictionary"/>.
/// </summary>
/// <param name="headers">The <see cref="IHeaderDictionary"/>.</param>
/// <param name="messages">The <see cref="MessageItemCollection"/>.</param>
/// <param name="jsonSerializer">The optional <see cref="IJsonSerializer"/>.</param>
public static void AddMessages(this IHeaderDictionary headers, MessageItemCollection? messages, IJsonSerializer? jsonSerializer = null)
{
if (messages is null || messages.Count == 0)
return;

jsonSerializer ??= JsonSerializer.Default;
headers.TryAdd(HttpConsts.MessagesHeaderName, jsonSerializer.Serialize(messages));
}
}
}
12 changes: 12 additions & 0 deletions src/CoreEx.AspNetCore/WebApis/ExtendedContentResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.AspNetCore.Http;
using CoreEx.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
Expand All @@ -13,6 +15,13 @@ namespace CoreEx.AspNetCore.WebApis
/// </summary>
public class ExtendedContentResult : ContentResult, IExtendedActionResult
{
/// <summary>
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders"/> <see cref="CoreEx.Http.HttpConsts.MessagesHeaderName"/> <see cref="MessageItemCollection"/>.
/// </summary>
/// <remarks>Defaults to the <see cref="ExecutionContext.Current"/> <see cref="ExecutionContext.Messages"/>.
/// <para><i>Note:</i> These are only written to the headers where the <see cref="ContentResult.StatusCode"/> is considered successful; i.e. is in the 200-299 range.</para></remarks>
public MessageItemCollection? Messages { get; set; } = ExecutionContext.HasCurrent && ExecutionContext.Current.HasMessages ? ExecutionContext.Current.Messages : null;

/// <inheritdoc/>
[JsonIgnore]
public Func<HttpResponse, Task>? BeforeExtension { get; set; }
Expand All @@ -24,6 +33,9 @@ public class ExtendedContentResult : ContentResult, IExtendedActionResult
/// <inheritdoc/>
public override async Task ExecuteResultAsync(ActionContext context)
{
if (StatusCode >= 200 || StatusCode <= 299)
context.HttpContext.Response.Headers.AddMessages(Messages);

if (BeforeExtension != null)
await BeforeExtension(context.HttpContext.Response).ConfigureAwait(false);

Expand Down
12 changes: 12 additions & 0 deletions src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.AspNetCore.Http;
using CoreEx.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
Expand All @@ -26,6 +28,13 @@ public ExtendedStatusCodeResult(HttpStatusCode statusCode) : this((int)statusCod
/// </summary>
public Uri? Location { get; set; }

/// <summary>
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders"/> <see cref="CoreEx.Http.HttpConsts.MessagesHeaderName"/> <see cref="MessageItemCollection"/>.
/// </summary>
/// <remarks>Defaults to the <see cref="ExecutionContext.Current"/> <see cref="ExecutionContext.Messages"/>.
/// <para><i>Note:</i> These are only written to the headers where the <see cref="StatusCodeResult.StatusCode"/> is considered successful; i.e. is in the 200-299 range.</para></remarks>
public MessageItemCollection? Messages { get; set; } = ExecutionContext.HasCurrent && ExecutionContext.Current.HasMessages ? ExecutionContext.Current.Messages : null;

/// <inheritdoc/>
[JsonIgnore]
public Func<HttpResponse, Task>? BeforeExtension { get; set; }
Expand All @@ -37,6 +46,9 @@ public ExtendedStatusCodeResult(HttpStatusCode statusCode) : this((int)statusCod
/// <inheritdoc/>
public override async Task ExecuteResultAsync(ActionContext context)
{
if (StatusCode >= 200 || StatusCode <= 299)
context.HttpContext.Response.Headers.AddMessages(Messages);

if (Location != null)
context.HttpContext.Response.GetTypedHeaders().Location = Location;

Expand Down
21 changes: 11 additions & 10 deletions src/CoreEx.Azure/Storage/BlobLeaseSynchronizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,6 @@ public void Exit<T>(string? name = null)
/// <inheritdoc/>
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
if (_timer.IsValueCreated)
_timer.Value.Dispose();

_dict.Values.ForEach(ReleaseLease);
}

Dispose(true);
GC.SuppressFinalize(this);
}
Expand All @@ -141,7 +132,17 @@ public void Dispose()
/// Releases the unmanaged resources used by the <see cref="BlobLeaseSynchronizer"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing) { }
protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
_disposed = true;
if (_timer.IsValueCreated)
_timer.Value.Dispose();

_dict.Values.ForEach(ReleaseLease);
}
}

/// <summary>
/// Gets the full name.
Expand Down
3 changes: 2 additions & 1 deletion src/CoreEx.Database/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ protected virtual void Dispose(bool disposing)
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}

Expand All @@ -203,6 +202,8 @@ public virtual async ValueTask DisposeAsyncCore()
await _dbConn.DisposeAsync().ConfigureAwait(false);
_dbConn = null;
}

Dispose();
}
}
}
21 changes: 10 additions & 11 deletions src/CoreEx.UnitTesting/AspNetCore/AgentTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public class AgentTester<TAgent>(TesterBase owner, TestServer testServer) : Http
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor"/>.</returns>
public HttpResponseMessageAssertor Run(Func<TAgent, Task<Ceh.HttpResult>> func) => RunAsync(func).GetAwaiter().GetResult();
/// <returns>An <see cref="HttpResultAssertor"/>.</returns>
public HttpResultAssertor Run(Func<TAgent, Task<Ceh.HttpResult>> func) => RunAsync(func).GetAwaiter().GetResult();

/// <summary>
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor"/>.</returns>
public HttpResponseMessageAssertor<TValue> Run<TValue>(Func<TAgent, Task<Ceh.HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();
/// <returns>An <see cref="HttpResultAssertor"/>.</returns>
public HttpResultAssertor<TValue> Run<TValue>(Func<TAgent, Task<Ceh.HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();

/// <summary>
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
Expand All @@ -44,8 +44,8 @@ public class AgentTester<TAgent>(TesterBase owner, TestServer testServer) : Http
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor"/>.</returns>
public async Task<HttpResponseMessageAssertor> RunAsync(Func<TAgent, Task<Ceh.HttpResult>> func)
/// <returns>An <see cref="HttpResultAssertor"/>.</returns>
public async Task<HttpResultAssertor> RunAsync(Func<TAgent, Task<Ceh.HttpResult>> func)
{
func.ThrowIfNull(nameof(func));

Expand All @@ -58,15 +58,15 @@ public async Task<HttpResponseMessageAssertor> RunAsync(Func<TAgent, Task<Ceh.Ht
var result = res.ToResult();
await ExpectationsArranger.AssertAsync(ExpectationsArranger.CreateArgs(LastLogs, result.IsFailure ? result.Error : null).AddExtra(res.Response)).ConfigureAwait(false);

return new HttpResponseMessageAssertor(Owner, res.Response);
return new HttpResultAssertor(Owner, res);
}

/// <summary>
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor"/>.</returns>
public async Task<HttpResponseMessageAssertor<TValue>> RunAsync<TValue>(Func<TAgent, Task<Ceh.HttpResult<TValue>>> func)
/// <returns>An <see cref="HttpResultAssertor"/>.</returns>
public async Task<HttpResultAssertor<TValue>> RunAsync<TValue>(Func<TAgent, Task<Ceh.HttpResult<TValue>>> func)
{
func.ThrowIfNull(nameof(func));

Expand All @@ -82,7 +82,7 @@ public async Task<HttpResponseMessageAssertor<TValue>> RunAsync<TValue>(Func<TAg
else
await ExpectationsArranger.AssertAsync(ExpectationsArranger.CreateArgs(LastLogs, result.Error).AddExtra(res.Response)).ConfigureAwait(false);

return res.IsSuccess ? new HttpResponseMessageAssertor<TValue>(Owner, res.Value, res.Response) : new HttpResponseMessageAssertor<TValue>(Owner, res.Response);
return res.IsSuccess ? new HttpResultAssertor<TValue>(Owner, res.Value, res) : new HttpResultAssertor<TValue>(Owner, res);
}

/// <summary>
Expand All @@ -103,7 +103,6 @@ public async Task<HttpResponseMessageAssertor> RunAsync(Func<TAgent, Task<HttpRe

return new HttpResponseMessageAssertor(Owner, res);
}

/// <summary>
/// Perform the assertion of any expectations.
/// </summary>
Expand Down
10 changes: 5 additions & 5 deletions src/CoreEx.UnitTesting/AspNetCore/AgentTesterT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public class AgentTester<TAgent, TValue>(TesterBase owner, TestServer testServer
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor{TValue}"/>.</returns>
public HttpResponseMessageAssertor<TValue> Run(Func<TAgent, Task<HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();
/// <returns>An <see cref="HttpResultAssertor{TValue}"/>.</returns>
public HttpResultAssertor<TValue> Run(Func<TAgent, Task<HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();

/// <summary>
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
Expand All @@ -39,8 +39,8 @@ public class AgentTester<TAgent, TValue>(TesterBase owner, TestServer testServer
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
/// </summary>
/// <param name="func">The function to execution.</param>
/// <returns>An <see cref="HttpResponseMessageAssertor{TValue}"/>.</returns>
public async Task<HttpResponseMessageAssertor<TValue>> RunAsync(Func<TAgent, Task<HttpResult<TValue>>> func)
/// <returns>An <see cref="HttpResultAssertor{TValue}"/>.</returns>
public async Task<HttpResultAssertor<TValue>> RunAsync(Func<TAgent, Task<HttpResult<TValue>>> func)
{
func.ThrowIfNull(nameof(func));

Expand All @@ -56,7 +56,7 @@ public async Task<HttpResponseMessageAssertor<TValue>> RunAsync(Func<TAgent, Tas
else
await ExpectationsArranger.AssertAsync(ExpectationsArranger.CreateArgs(LastLogs, result.Error).AddExtra(res.Response)).ConfigureAwait(false);

return res.IsSuccess ? new HttpResponseMessageAssertor<TValue>(Owner, res.Value, res.Response) : new HttpResponseMessageAssertor<TValue>(Owner, res.Response);
return res.IsSuccess ? new HttpResultAssertor<TValue>(Owner, res.Value, res) : new HttpResultAssertor<TValue>(Owner, res);
}

/// <summary>
Expand Down
21 changes: 21 additions & 0 deletions src/CoreEx.UnitTesting/Assertors/HttpResultAssertor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx;
using CoreEx.Http;
using UnitTestEx.Abstractions;

namespace UnitTestEx.Assertors
{
/// <summary>
/// Represents the <see cref="HttpResult"/> test assert helper.
/// </summary>
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
/// <param name="result">The <see cref="HttpResult"/>.</param>
public class HttpResultAssertor(TesterBase owner, HttpResult result) : HttpResponseMessageAssertor(owner, result.ThrowIfNull(nameof(result)).Response)
{
/// <summary>
/// Gets the <see cref="HttpResult"/>.
/// </summary>
public HttpResult Result { get; private set; } = result;
}
}
36 changes: 36 additions & 0 deletions src/CoreEx.UnitTesting/Assertors/HttpResultAssertorT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx;
using CoreEx.Http;
using System;
using UnitTestEx.Abstractions;

namespace UnitTestEx.Assertors
{
/// <summary>
/// Represents the <see cref="HttpResult{TValue}"/> test assert helper with a specified result <typeparamref name="TValue"/> <see cref="Type"/>.
/// </summary>
///
public class HttpResultAssertor<TValue> : HttpResponseMessageAssertor<TValue>
{
/// <summary>
/// Initializes a new instance of the <see cref="HttpResultAssertor"/> class.
/// </summary>
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
/// <param name="result">The <see cref="HttpResult"/>.</param>
public HttpResultAssertor(TesterBase owner, HttpResult<TValue> result) : base(owner, result.ThrowIfNull(nameof(result)).Response) => Result = result;

/// <summary>
/// Initializes a new instance of the <see cref="HttpResultAssertor"/> class.
/// </summary>
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
/// <param name="value">The value already deserialized.</param>
/// <param name="result"></param>
public HttpResultAssertor(TesterBase owner, TValue value, HttpResult<TValue> result) : base(owner, value, result.ThrowIfNull(nameof(result)).Response) => Result = result;

/// <summary>
/// Gets the <see cref="HttpResult{TValue}"/>.
/// </summary>
public HttpResult<TValue> Result { get; private set; }
}
}
2 changes: 1 addition & 1 deletion src/CoreEx/Entities/MessageItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static MessageItem CreateErrorMessage(string property, LText format, para
#endregion

/// <summary>
/// Gets the message severity validatorType.
/// Gets the message severity type.
/// </summary>
public MessageType Type { get; set; }

Expand Down
Loading

0 comments on commit 1dfcbda

Please sign in to comment.