Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.15.0 #95

Merged
merged 1 commit into from
Mar 25, 2024
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

Represents the **NuGet** versions.

## v3.15.0
- *Enhancement*: This is a clean-up version to remove all obsolete code and dependencies. This will result in a number of minor breaking changes, but will ensure that the codebase is up-to-date and maintainable.
- As per [`v3.14.0`](#v3.14.0) the previously obsoleted `TypedHttpClientBase` methods `WithRetry`, `WithTimeout`, `WithCustomRetryPolicy` and `WithMaxRetryDelay` are now removed; including `TypedHttpClientOptions`, `HttpRequestLogger` and related `SettingsBase` capabilities.
- Health checks:
- `CoreEx.Azure.HealthChecks` namespace and classes removed.
- `SqlServerHealthCheck` replaced with simple generic `DatabaseHealthCheck`.
- `IServiceCollection.AddDatabase` automatically adds `DatabaseHealthCheck`.
- `IServiceCollection.AddSqlServerEventOutboxHostedService` automatically adds `TimerHostedServiceHealthCheck`.
- `IServiceCollection.AddReferenceDataOrchestrator` automatically adds `ReferenceDataOrchestratorHealthCheck` (reports cache statistics).
- `HealthReportStatusWriter` added to support richer JSON reporting.
- Generally recommend using 3rd-party library to enable further health checks; for example: [`https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks`](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks).

## v3.14.1
- *Fixed*: The `Result.ValidatesAsync` extension method signature has had the value nullability corrected to enable fluent-style method-chaining.
- *Fixed*: The fully qualified type and property name is now correctly used as the `LText.KeyAndOrText` when creating within the `PropertyExpression<TEntity, TProperty>` to enable a qualified _key_ that can be used by the `ITextProvider` to substitute the text at runtime; the existing text fallback behavior remains such that an appropriate text is used. The `PropertyExpression.CreatePropertyLTextKey` function can be overridden to change this behavior.
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.14.1</Version>
<Version>3.15.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
23 changes: 0 additions & 23 deletions samples/My.Hr/My.Hr.Api/Controllers/HealthController.cs

This file was deleted.

1 change: 0 additions & 1 deletion samples/My.Hr/My.Hr.Api/ImplicitUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
global using CoreEx.Configuration;
global using CoreEx.Entities;
global using CoreEx.Events;
global using CoreEx.HealthChecks;
global using CoreEx.Http;
global using CoreEx.Json;
global using CoreEx.RefData;
Expand Down
12 changes: 5 additions & 7 deletions samples/My.Hr/My.Hr.Api/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using CoreEx.Azure.HealthChecks;
using CoreEx.Database;
using CoreEx.DataBase.HealthChecks;
using CoreEx.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Azure.Monitor.OpenTelemetry.AspNetCore;
using OpenTelemetry.Trace;
using Az = Azure.Messaging.ServiceBus;
using CoreEx.Database.HealthChecks;

namespace My.Hr.Api;

Expand Down Expand Up @@ -52,10 +51,8 @@ public void ConfigureServices(IServiceCollection services)

// Register the health checks.
services
.AddScoped<HealthService>()
.AddHealthChecks()
.AddTypeActivatedCheck<AzureServiceBusQueueHealthCheck>("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName))
.AddTypeActivatedCheck<SqlServerHealthCheck>("SQL Server", HealthStatus.Unhealthy, tags: default!, timeout: TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database));
.AddHealthChecks();
//.AddTypeActivatedCheck<AzureServiceBusQueueHealthCheck>("Verification Queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName))

services.AddControllers();

Expand Down Expand Up @@ -90,6 +87,7 @@ public void Configure(IApplicationBuilder app)
.UseRouting()
.UseAuthorization()
.UseExecutionContext()
.UseHealthChecks("/healthz", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { ResponseWriter = HealthReportStatusWriter.WriteJsonResults })
.UseReferenceDataOrchestrator()
.UseEndpoints(endpoints => endpoints.MapControllers());
}
9 changes: 4 additions & 5 deletions samples/My.Hr/My.Hr.Business/External/AgifyServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace My.Hr.Business.External;
/// </summary>
public class AgifyApiClient : TypedHttpClientCore<AgifyApiClient>
{
public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger<TypedHttpClientCore<AgifyApiClient>> logger)
: base(client, jsonSerializer, executionContext, settings, logger)
public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings)
: base(client, jsonSerializer, executionContext)
{
if (!Uri.IsWellFormedUriString(settings.AgifyApiEndpointUri, UriKind.Absolute))
throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.AgifyApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.AgifyApiEndpointUri)}'.
Expand All @@ -17,14 +17,13 @@ public AgifyApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.

public override Task<HttpResult> HealthCheckAsync(CancellationToken cancellationToken)
{
return base.HeadAsync(string.Empty, null, new HttpArg<string>[] { new HttpArg<string>("name", "health") }, cancellationToken);
return base.HeadAsync(string.Empty, null, new HttpArg<string>[] { new("name", "health") }, cancellationToken);
}

public async Task<AgifyResponse> GetAgeAsync(string name)
{
var response = await
WithRetry(1, 5)
.ThrowTransientException()
ThrowTransientException()
.GetAsync<AgifyResponse>(string.Empty, null, HttpArgs.Create(new HttpArg<string>("name", name)));

return response.Value;
Expand Down
7 changes: 3 additions & 4 deletions samples/My.Hr/My.Hr.Business/External/GenderizeApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace My.Hr.Business.External;
/// </summary>
public class GenderizeApiClient : TypedHttpClientCore<GenderizeApiClient>
{
public GenderizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger<TypedHttpClientCore<GenderizeApiClient>> logger)
: base(client, jsonSerializer, executionContext, settings, logger)
public GenderizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings)
: base(client, jsonSerializer, executionContext)
{
if (!Uri.IsWellFormedUriString(settings.GenderizeApiClientApiEndpointUri, UriKind.Absolute))
throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.GenderizeApiClientApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.GenderizeApiClientApiEndpointUri)}'.
Expand All @@ -23,8 +23,7 @@ public override Task<HttpResult> HealthCheckAsync(CancellationToken cancellation
public async Task<GenderizeResponse> GetGenderAsync(string name)
{
var response = await
WithRetry(1, 5)
.ThrowTransientException()
ThrowTransientException()
.GetAsync<GenderizeResponse>(string.Empty, null, HttpArgs.Create(new HttpArg<string>("name", name)));

return response.Value;
Expand Down
7 changes: 3 additions & 4 deletions samples/My.Hr/My.Hr.Business/External/NationalizeApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ namespace My.Hr.Business.External;
/// </summary>
public class NationalizeApiClient : TypedHttpClientCore<NationalizeApiClient>
{
public NationalizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings, ILogger<TypedHttpClientCore<NationalizeApiClient>> logger)
: base(client, jsonSerializer, executionContext, settings, logger)
public NationalizeApiClient(HttpClient client, IJsonSerializer jsonSerializer, CoreEx.ExecutionContext executionContext, HrSettings settings)
: base(client, jsonSerializer, executionContext)
{
if (!Uri.IsWellFormedUriString(settings.NationalizeApiClientApiEndpointUri, UriKind.Absolute))
throw new InvalidOperationException(@$"The Api endpoint URI is not valid: {settings.NationalizeApiClientApiEndpointUri}. Provide valid Api endpoint URI in the configuration '{nameof(settings.NationalizeApiClientApiEndpointUri)}'.
Expand All @@ -23,8 +23,7 @@ public override Task<HttpResult> HealthCheckAsync(CancellationToken cancellation
public async Task<NationalizeResponse> GetNationalityAsync(string name)
{
var response = await
WithRetry(1, 5)
.ThrowTransientException()
ThrowTransientException()
.GetAsync<NationalizeResponse>(string.Empty, null, HttpArgs.Create(new HttpArg<string>("name", name)));

return response.Value;
Expand Down
31 changes: 0 additions & 31 deletions samples/My.Hr/My.Hr.Functions/Functions/HttpHealthFunction.cs

This file was deleted.

10 changes: 4 additions & 6 deletions samples/My.Hr/My.Hr.Functions/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
using CoreEx;
using CoreEx.AspNetCore.HealthChecks;
using CoreEx.AspNetCore.WebApis;
using CoreEx.Azure.HealthChecks;
using CoreEx.Database;
using CoreEx.DataBase.HealthChecks;
using CoreEx.HealthChecks;
using CoreEx.Database.HealthChecks;
using CoreEx.Http.HealthChecks;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -48,13 +47,12 @@ public override void Configure(IFunctionsHostBuilder builder)

// Register the health checks.
builder.Services
.AddScoped<HealthService>()
.AddHealthChecks()
.AddTypeActivatedCheck<TypedHttpClientCoreHealthCheck<GenderizeApiClient>>("Genderize API")
.AddTypeActivatedCheck<TypedHttpClientCoreHealthCheck<AgifyApiClient>>("Agify API")
.AddTypeActivatedCheck<TypedHttpClientCoreHealthCheck<NationalizeApiClient>>("Nationalize API")
.AddTypeActivatedCheck<AzureServiceBusQueueHealthCheck>("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName))
.AddTypeActivatedCheck<SqlServerHealthCheck>("SQL Server", HealthStatus.Unhealthy, tags: default, timeout: System.TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database));
//.AddTypeActivatedCheck<AzureServiceBusQueueHealthCheck>("Health check for service bus verification queue", HealthStatus.Unhealthy, nameof(HrSettings.ServiceBusConnection), nameof(HrSettings.VerificationQueueName))
.AddTypeActivatedCheck<DatabaseHealthCheck<IDatabase>>("SQL Server", HealthStatus.Unhealthy, tags: default, timeout: System.TimeSpan.FromSeconds(15), nameof(HrSettings.ConnectionStrings__Database));

// Register the business services.
builder.Services
Expand Down
113 changes: 113 additions & 0 deletions src/CoreEx.AspNetCore/HealthChecks/HealthReportStatusWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System;
using System.IO;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace CoreEx.AspNetCore.HealthChecks
{
/// <summary>
/// Provides additional <see cref="HealthReport"/> <c>HealthCheckOptions.ResponseWriter</c> capabilities.
/// </summary>
public static class HealthReportStatusWriter
{
/// <summary>
/// Writes the <paramref name="healthReport"/> as a JSON summary.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="healthReport">The <see cref="HealthReport"/>.</param>
public static Task WriteJsonSummary(HttpContext context, HealthReport healthReport) => WriteJson(context, healthReport, false, false, null);

/// <summary>
/// Writes the <paramref name="healthReport"/> as JSON including the <see cref="HealthReport.Entries"/> results.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="healthReport">The <see cref="HealthReport"/>.</param>
public static Task WriteJsonResults(HttpContext context, HealthReport healthReport) => WriteJson(context, healthReport, true, false, null);

/// <summary>
/// Writes the <paramref name="healthReport"/> as JSON including the <see cref="SettingsBase.Deployment"/> and <see cref="HealthReport.Entries"/> results.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="healthReport">The <see cref="HealthReport"/>.</param>
public static async Task WriteJsonDeploymentResults(HttpContext context, HealthReport healthReport) => await WriteJson(context, healthReport, true, true, null).ConfigureAwait(false);

/// <summary>
/// Writes the <paramref name="healthReport"/> as JSON to the <paramref name="context"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <param name="healthReport">The <see cref="HealthReport"/>.</param>
/// <param name="includeResults">Indicates whether to include <see cref="HealthReport.Entries"/> results (where applicable).</param>
/// <param name="includeDeployment">Indicates whether to include <see cref="SettingsBase.Deployment"/> information (where applicable).</param>
/// <param name="extension">An action to enable extensions to the underlying JSON being written.</param>
public static async Task WriteJson(HttpContext context, HealthReport healthReport, bool includeResults = true, bool includeDeployment = true, Action<HealthReport, Utf8JsonWriter>? extension = null)
{
using var memoryStream = new MemoryStream();
using (var jsonWriter = new Utf8JsonWriter(memoryStream))
{
jsonWriter.WriteStartObject();
jsonWriter.WriteString("status", healthReport.Status.ToString());
jsonWriter.WriteString("duration", healthReport.TotalDuration.ToString());

if (ExecutionContext.HasCurrent)
jsonWriter.WriteString("correlationId", ExecutionContext.Current.CorrelationId);

jsonWriter.WriteStartObject("results");

foreach (var e in healthReport.Entries)
{
jsonWriter.WriteStartObject(e.Key.Replace(' ', '-'));
jsonWriter.WriteString("status", e.Value.Status.ToString());
jsonWriter.WriteString("description", e.Value.Description);
jsonWriter.WriteString("duration", e.Value.Duration.ToString());

if (e.Value.Exception is not null)
{
var settings = ExecutionContext.GetService<SettingsBase>();
if (settings is not null && settings.IncludeExceptionInResult)
jsonWriter.WriteString("exception", e.Value.Exception?.Message);
}

if (includeDeployment)
{
var settings = ExecutionContext.GetService<SettingsBase>();
if (settings is not null)
{
jsonWriter.WritePropertyName("deployment");
JsonSerializer.Serialize(jsonWriter, settings, Text.Json.JsonSerializer.DefaultOptions);
}
}

if (includeResults && e.Value.Data.Count > 0)
{
jsonWriter.WriteStartObject("data");

foreach (var d in e.Value.Data)
{
jsonWriter.WritePropertyName(d.Key);
JsonSerializer.Serialize(jsonWriter, d.Value, d.Value?.GetType() ?? typeof(object), Text.Json.JsonSerializer.DefaultOptions);
}

jsonWriter.WriteEndObject();
}

jsonWriter.WriteEndObject();
}

extension?.Invoke(healthReport, jsonWriter);

jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}

context.Response.ContentType = MediaTypeNames.Application.Json;
await context.Response.WriteAsync(Encoding.UTF8.GetString(memoryStream.ToArray())).ConfigureAwait(false);
}
}
}
Loading
Loading