Skip to content

Commit 1dfcbda

Browse files
authored
v3.27.3 (#129)
* 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.
1 parent ce9f2dc commit 1dfcbda

File tree

22 files changed

+313
-60
lines changed

22 files changed

+313
-60
lines changed

CHANGELOG.md

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

33
Represents the **NuGet** versions.
44

5+
## v3.27.3
6+
- *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.
7+
- *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.
8+
59
## v3.27.2
610
- *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).
711

Common.targets

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

samples/My.Hr/My.Hr.Business/Services/EmployeeService.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@ public EmployeeService(HrDbContext dbContext, IEventPublisher publisher, HrSetti
3131
_settings = settings;
3232
}
3333

34-
public async Task<Employee?> GetEmployeeAsync(Guid id)
35-
=> await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id);
34+
public async Task<Employee?> GetEmployeeAsync(Guid id)
35+
{
36+
var emp = await _dbContext.Employees.FirstOrDefaultAsync(e => e.Id == id);
37+
if (emp is not null && emp.Birthday.HasValue && emp.Birthday.Value.Year < 2000)
38+
CoreEx.ExecutionContext.Current.Messages.Add(MessageType.Warning, "Employee is considered old.");
39+
40+
return emp;
41+
}
3642

3743
public Task<EmployeeCollectionResult> GetAllAsync(QueryArgs? query, PagingArgs? paging)
3844
=> _dbContext.Employees.Where(_queryConfig, query).OrderBy(_queryConfig, query).ToCollectionResultAsync<EmployeeCollectionResult, EmployeeCollection, Employee>(paging);

samples/My.Hr/My.Hr.UnitTest/EmployeeControllerTest.cs

+17-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void A110_Get_Found()
5151
{
5252
using var test = ApiTester.Create<Startup>();
5353

54-
test.Controller<EmployeeController>()
54+
var resp = test.Controller<EmployeeController>()
5555
.Run(c => c.GetAsync(1.ToGuid()))
5656
.AssertOK()
5757
.AssertValue(new Employee
@@ -64,7 +64,22 @@ public void A110_Get_Found()
6464
Birthday = new DateTime(1985, 03, 18, 0, 0, 0, DateTimeKind.Unspecified),
6565
StartDate = new DateTime(2000, 12, 11, 0, 0, 0, DateTimeKind.Unspecified),
6666
PhoneNo = "(425) 612 8113"
67-
}, nameof(Employee.ETag));
67+
}, nameof(Employee.ETag))
68+
.Response;
69+
70+
// Also, validate the context header messages.
71+
var result = HttpResult.CreateAsync(resp).GetAwaiter().GetResult();
72+
Assert.Multiple(() =>
73+
{
74+
Assert.That(result.IsSuccess, Is.True);
75+
Assert.That(result.Messages, Is.Not.Null);
76+
});
77+
Assert.That(result.Messages, Has.Count.EqualTo(1));
78+
Assert.Multiple(() =>
79+
{
80+
Assert.That(result.Messages[0].Type, Is.EqualTo(MessageType.Warning));
81+
Assert.That(result.Messages[0].Text, Is.EqualTo("Employee is considered old."));
82+
});
6883
}
6984

7085
[Test]

src/CoreEx.AspNetCore/Http/HttpResultExtensions.cs

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.Http;
1010
using Microsoft.Net.Http.Headers;
1111
using System;
12+
using System.Collections.Generic;
1213
using System.Globalization;
1314
using System.IO;
1415
using System.Net;
@@ -172,5 +173,20 @@ public static void AddPagingResult(this IHeaderDictionary headers, PagingResult?
172173
if (paging.TotalPages.HasValue)
173174
headers[HttpConsts.PagingTotalPagesHeaderName] = paging.TotalPages.Value.ToString(CultureInfo.InvariantCulture);
174175
}
176+
177+
/// <summary>
178+
/// Adds the <see cref="MessageItemCollection"/> to the <see cref="IHeaderDictionary"/>.
179+
/// </summary>
180+
/// <param name="headers">The <see cref="IHeaderDictionary"/>.</param>
181+
/// <param name="messages">The <see cref="MessageItemCollection"/>.</param>
182+
/// <param name="jsonSerializer">The optional <see cref="IJsonSerializer"/>.</param>
183+
public static void AddMessages(this IHeaderDictionary headers, MessageItemCollection? messages, IJsonSerializer? jsonSerializer = null)
184+
{
185+
if (messages is null || messages.Count == 0)
186+
return;
187+
188+
jsonSerializer ??= JsonSerializer.Default;
189+
headers.TryAdd(HttpConsts.MessagesHeaderName, jsonSerializer.Serialize(messages));
190+
}
175191
}
176192
}

src/CoreEx.AspNetCore/WebApis/ExtendedContentResult.cs

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
22

3+
using CoreEx.AspNetCore.Http;
4+
using CoreEx.Entities;
35
using Microsoft.AspNetCore.Http;
46
using Microsoft.AspNetCore.Mvc;
57
using System;
@@ -13,6 +15,13 @@ namespace CoreEx.AspNetCore.WebApis
1315
/// </summary>
1416
public class ExtendedContentResult : ContentResult, IExtendedActionResult
1517
{
18+
/// <summary>
19+
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders"/> <see cref="CoreEx.Http.HttpConsts.MessagesHeaderName"/> <see cref="MessageItemCollection"/>.
20+
/// </summary>
21+
/// <remarks>Defaults to the <see cref="ExecutionContext.Current"/> <see cref="ExecutionContext.Messages"/>.
22+
/// <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>
23+
public MessageItemCollection? Messages { get; set; } = ExecutionContext.HasCurrent && ExecutionContext.Current.HasMessages ? ExecutionContext.Current.Messages : null;
24+
1625
/// <inheritdoc/>
1726
[JsonIgnore]
1827
public Func<HttpResponse, Task>? BeforeExtension { get; set; }
@@ -24,6 +33,9 @@ public class ExtendedContentResult : ContentResult, IExtendedActionResult
2433
/// <inheritdoc/>
2534
public override async Task ExecuteResultAsync(ActionContext context)
2635
{
36+
if (StatusCode >= 200 || StatusCode <= 299)
37+
context.HttpContext.Response.Headers.AddMessages(Messages);
38+
2739
if (BeforeExtension != null)
2840
await BeforeExtension(context.HttpContext.Response).ConfigureAwait(false);
2941

src/CoreEx.AspNetCore/WebApis/ExtendedStatusCodeResult.cs

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
22

3+
using CoreEx.AspNetCore.Http;
4+
using CoreEx.Entities;
35
using Microsoft.AspNetCore.Http;
46
using Microsoft.AspNetCore.Mvc;
57
using System;
@@ -26,6 +28,13 @@ public ExtendedStatusCodeResult(HttpStatusCode statusCode) : this((int)statusCod
2628
/// </summary>
2729
public Uri? Location { get; set; }
2830

31+
/// <summary>
32+
/// Gets or sets the <see cref="Microsoft.AspNetCore.Http.Headers.ResponseHeaders"/> <see cref="CoreEx.Http.HttpConsts.MessagesHeaderName"/> <see cref="MessageItemCollection"/>.
33+
/// </summary>
34+
/// <remarks>Defaults to the <see cref="ExecutionContext.Current"/> <see cref="ExecutionContext.Messages"/>.
35+
/// <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>
36+
public MessageItemCollection? Messages { get; set; } = ExecutionContext.HasCurrent && ExecutionContext.Current.HasMessages ? ExecutionContext.Current.Messages : null;
37+
2938
/// <inheritdoc/>
3039
[JsonIgnore]
3140
public Func<HttpResponse, Task>? BeforeExtension { get; set; }
@@ -37,6 +46,9 @@ public ExtendedStatusCodeResult(HttpStatusCode statusCode) : this((int)statusCod
3746
/// <inheritdoc/>
3847
public override async Task ExecuteResultAsync(ActionContext context)
3948
{
49+
if (StatusCode >= 200 || StatusCode <= 299)
50+
context.HttpContext.Response.Headers.AddMessages(Messages);
51+
4052
if (Location != null)
4153
context.HttpContext.Response.GetTypedHeaders().Location = Location;
4254

src/CoreEx.Azure/Storage/BlobLeaseSynchronizer.cs

+11-10
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,6 @@ public void Exit<T>(string? name = null)
124124
/// <inheritdoc/>
125125
public void Dispose()
126126
{
127-
if (!_disposed)
128-
{
129-
_disposed = true;
130-
if (_timer.IsValueCreated)
131-
_timer.Value.Dispose();
132-
133-
_dict.Values.ForEach(ReleaseLease);
134-
}
135-
136127
Dispose(true);
137128
GC.SuppressFinalize(this);
138129
}
@@ -141,7 +132,17 @@ public void Dispose()
141132
/// Releases the unmanaged resources used by the <see cref="BlobLeaseSynchronizer"/> and optionally releases the managed resources.
142133
/// </summary>
143134
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
144-
protected virtual void Dispose(bool disposing) { }
135+
protected virtual void Dispose(bool disposing)
136+
{
137+
if (disposing && !_disposed)
138+
{
139+
_disposed = true;
140+
if (_timer.IsValueCreated)
141+
_timer.Value.Dispose();
142+
143+
_dict.Values.ForEach(ReleaseLease);
144+
}
145+
}
145146

146147
/// <summary>
147148
/// Gets the full name.

src/CoreEx.Database/Database.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ protected virtual void Dispose(bool disposing)
187187
public async ValueTask DisposeAsync()
188188
{
189189
await DisposeAsyncCore().ConfigureAwait(false);
190-
Dispose(false);
191190
GC.SuppressFinalize(this);
192191
}
193192

@@ -203,6 +202,8 @@ public virtual async ValueTask DisposeAsyncCore()
203202
await _dbConn.DisposeAsync().ConfigureAwait(false);
204203
_dbConn = null;
205204
}
205+
206+
Dispose();
206207
}
207208
}
208209
}

src/CoreEx.UnitTesting/AspNetCore/AgentTester.cs

+10-11
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ public class AgentTester<TAgent>(TesterBase owner, TestServer testServer) : Http
2323
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
2424
/// </summary>
2525
/// <param name="func">The function to execution.</param>
26-
/// <returns>An <see cref="HttpResponseMessageAssertor"/>.</returns>
27-
public HttpResponseMessageAssertor Run(Func<TAgent, Task<Ceh.HttpResult>> func) => RunAsync(func).GetAwaiter().GetResult();
26+
/// <returns>An <see cref="HttpResultAssertor"/>.</returns>
27+
public HttpResultAssertor Run(Func<TAgent, Task<Ceh.HttpResult>> func) => RunAsync(func).GetAwaiter().GetResult();
2828

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

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

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

61-
return new HttpResponseMessageAssertor(Owner, res.Response);
61+
return new HttpResultAssertor(Owner, res);
6262
}
6363

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

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

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

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

104104
return new HttpResponseMessageAssertor(Owner, res);
105105
}
106-
107106
/// <summary>
108107
/// Perform the assertion of any expectations.
109108
/// </summary>

src/CoreEx.UnitTesting/AspNetCore/AgentTesterT.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public class AgentTester<TAgent, TValue>(TesterBase owner, TestServer testServer
2525
/// Runs the test by executing a <typeparamref name="TAgent"/> method.
2626
/// </summary>
2727
/// <param name="func">The function to execution.</param>
28-
/// <returns>An <see cref="HttpResponseMessageAssertor{TValue}"/>.</returns>
29-
public HttpResponseMessageAssertor<TValue> Run(Func<TAgent, Task<HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();
28+
/// <returns>An <see cref="HttpResultAssertor{TValue}"/>.</returns>
29+
public HttpResultAssertor<TValue> Run(Func<TAgent, Task<HttpResult<TValue>>> func) => RunAsync(func).GetAwaiter().GetResult();
3030

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

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

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

6262
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
2+
3+
using CoreEx;
4+
using CoreEx.Http;
5+
using UnitTestEx.Abstractions;
6+
7+
namespace UnitTestEx.Assertors
8+
{
9+
/// <summary>
10+
/// Represents the <see cref="HttpResult"/> test assert helper.
11+
/// </summary>
12+
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
13+
/// <param name="result">The <see cref="HttpResult"/>.</param>
14+
public class HttpResultAssertor(TesterBase owner, HttpResult result) : HttpResponseMessageAssertor(owner, result.ThrowIfNull(nameof(result)).Response)
15+
{
16+
/// <summary>
17+
/// Gets the <see cref="HttpResult"/>.
18+
/// </summary>
19+
public HttpResult Result { get; private set; } = result;
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx
2+
3+
using CoreEx;
4+
using CoreEx.Http;
5+
using System;
6+
using UnitTestEx.Abstractions;
7+
8+
namespace UnitTestEx.Assertors
9+
{
10+
/// <summary>
11+
/// Represents the <see cref="HttpResult{TValue}"/> test assert helper with a specified result <typeparamref name="TValue"/> <see cref="Type"/>.
12+
/// </summary>
13+
///
14+
public class HttpResultAssertor<TValue> : HttpResponseMessageAssertor<TValue>
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="HttpResultAssertor"/> class.
18+
/// </summary>
19+
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
20+
/// <param name="result">The <see cref="HttpResult"/>.</param>
21+
public HttpResultAssertor(TesterBase owner, HttpResult<TValue> result) : base(owner, result.ThrowIfNull(nameof(result)).Response) => Result = result;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="HttpResultAssertor"/> class.
25+
/// </summary>
26+
/// <param name="owner">The owning <see cref="TesterBase"/>.</param>
27+
/// <param name="value">The value already deserialized.</param>
28+
/// <param name="result"></param>
29+
public HttpResultAssertor(TesterBase owner, TValue value, HttpResult<TValue> result) : base(owner, value, result.ThrowIfNull(nameof(result)).Response) => Result = result;
30+
31+
/// <summary>
32+
/// Gets the <see cref="HttpResult{TValue}"/>.
33+
/// </summary>
34+
public HttpResult<TValue> Result { get; private set; }
35+
}
36+
}

src/CoreEx/Entities/MessageItem.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static MessageItem CreateErrorMessage(string property, LText format, para
7272
#endregion
7373

7474
/// <summary>
75-
/// Gets the message severity validatorType.
75+
/// Gets the message severity type.
7676
/// </summary>
7777
public MessageType Type { get; set; }
7878

0 commit comments

Comments
 (0)