Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Represents the **NuGet** versions.

## v3.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