diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e351ce..e5957e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +Add extension to EFCore package to expose DB sync configuration in an HTTP endpoint in an ASP.NET Core app. + ## 0.5.2 Replace sync mode configuration with an option to the deploy CLI task. diff --git a/README.md b/README.md index 84f0a31..8c20efe 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,29 @@ If all is configured correctly, whenever any of the applications in your solutio a `dfe-analytics` directory should be created in the output directory. This directory contains a `config.json` file with the configuration for your DbContext, including which tables and columns are configured to sync to BigQuery. +### Exposing the sync configuration over HTTP + +If your DbContext is hosted in an ASP.NET Core application, you can expose its sync configuration over an HTTP endpoint. +This is useful for verifying at runtime which tables and columns are configured to sync to BigQuery. + +Map the endpoint in your application's entry point (e.g. `Program.cs`): + +```cs +using Dfe.Analytics.EFCore; + +//... +app.MapDfeAnalyticsDbConfiguration(); +``` + +By default the configuration is served as JSON from `/_dfe-analytics/db-config.json`. +You can supply a custom path if you prefer: + +```cs +app.MapDfeAnalyticsDbConfiguration("/my-custom-path.json"); +``` + +The `MyDbContext` type must be registered in the application's service provider (e.g. via `AddDbContext()`). + ## Capturing web request events in ASP.NET Core diff --git a/src/Dfe.Analytics.EFCore/AspNetCoreExtensions.cs b/src/Dfe.Analytics.EFCore/AspNetCoreExtensions.cs new file mode 100644 index 0000000..eaf111a --- /dev/null +++ b/src/Dfe.Analytics.EFCore/AspNetCoreExtensions.cs @@ -0,0 +1,39 @@ +using Dfe.Analytics.EFCore.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Dfe.Analytics.EFCore; + +public static class AspNetCoreExtensions +{ + private const string DefaultConfigurationPath = "/_dfe-analytics/db-config.json"; + + public static IEndpointConventionBuilder MapDfeAnalyticsDbConfiguration( + this IEndpointRouteBuilder endpointBuilder) + where TDbContext : DbContext + { + return MapDfeAnalyticsDbConfiguration(endpointBuilder, DefaultConfigurationPath); + } + + public static IEndpointConventionBuilder MapDfeAnalyticsDbConfiguration( + this IEndpointRouteBuilder endpointBuilder, + string path) + where TDbContext : DbContext + { + ArgumentNullException.ThrowIfNull(endpointBuilder); + ArgumentNullException.ThrowIfNull(path); + + return endpointBuilder.MapGet( + path, + ctx => + { + var configurationProvider = new AnalyticsConfigurationProvider(); + var dbContext = ctx.RequestServices.GetRequiredService(); + var configuration = configurationProvider.GetConfiguration(dbContext); + return ctx.Response.WriteAsJsonAsync(configuration, DatabaseSyncConfiguration.JsonSerializerOptions); + }); + } +} diff --git a/src/Dfe.Analytics.EFCore/Configuration/DatabaseSyncConfiguration.cs b/src/Dfe.Analytics.EFCore/Configuration/DatabaseSyncConfiguration.cs index 4c641fd..0377b0b 100644 --- a/src/Dfe.Analytics.EFCore/Configuration/DatabaseSyncConfiguration.cs +++ b/src/Dfe.Analytics.EFCore/Configuration/DatabaseSyncConfiguration.cs @@ -4,10 +4,7 @@ namespace Dfe.Analytics.EFCore.Configuration; public sealed class DatabaseSyncConfiguration : IEquatable { - private static readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web) - { - WriteIndented = true - }; + internal static JsonSerializerOptions JsonSerializerOptions { get; } = new(JsonSerializerDefaults.Web) { WriteIndented = true }; public required string DbContextName { get; init; } @@ -24,7 +21,7 @@ public static async Task ReadFromFileAsync(string con var json = await File.ReadAllTextAsync(configurationPath, cancellationToken); - return JsonSerializer.Deserialize(json, _jsonSerializerOptions) + return JsonSerializer.Deserialize(json, JsonSerializerOptions) ?? throw new InvalidOperationException("Failed to deserialize DatabaseSyncConfiguration."); } @@ -32,7 +29,7 @@ public Task WriteToFileAsync(string configurationPath, CancellationToken cancell { ArgumentNullException.ThrowIfNull(configurationPath); - var json = JsonSerializer.Serialize(this, _jsonSerializerOptions); + var json = JsonSerializer.Serialize(this, JsonSerializerOptions); return File.WriteAllTextAsync(configurationPath, json, cancellationToken); } diff --git a/src/Dfe.Analytics.EFCore/Dfe.Analytics.EFCore.csproj b/src/Dfe.Analytics.EFCore/Dfe.Analytics.EFCore.csproj index 6f3c561..c8d4d47 100644 --- a/src/Dfe.Analytics.EFCore/Dfe.Analytics.EFCore.csproj +++ b/src/Dfe.Analytics.EFCore/Dfe.Analytics.EFCore.csproj @@ -14,7 +14,7 @@ - + @@ -46,6 +46,7 @@ + diff --git a/tests/Dfe.Analytics.EFCore.Tests/AspNetCoreExtensionsTests.cs b/tests/Dfe.Analytics.EFCore.Tests/AspNetCoreExtensionsTests.cs new file mode 100644 index 0000000..63a20d8 --- /dev/null +++ b/tests/Dfe.Analytics.EFCore.Tests/AspNetCoreExtensionsTests.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Text.Json; +using Dfe.Analytics.EFCore.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Dfe.Analytics.EFCore.Tests; + +public class AspNetCoreExtensionsTests +{ + [Fact] + public async Task MapDfeAnalyticsDbConfiguration_ReturnsConfigurationForDbContext() + { + // Arrange + using var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRouting(); + services.AddScoped(_ => new TestDbContext()); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapDfeAnalyticsDbConfiguration("/db-configuration"); + }); + }); + }) + .StartAsync(TestContext.Current.CancellationToken); + + var client = host.GetTestClient(); + + // Act + var response = await client.GetAsync("/db-configuration", TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var json = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); + var configuration = JsonSerializer.Deserialize( + json, + DatabaseSyncConfiguration.JsonSerializerOptions); + + var expectedConfiguration = new AnalyticsConfigurationProvider().GetConfiguration(new TestDbContext()); + + Assert.NotNull(configuration); + Assert.Equal(expectedConfiguration, configuration); + } +} diff --git a/tests/Dfe.Analytics.EFCore.Tests/Dfe.Analytics.EFCore.Tests.csproj b/tests/Dfe.Analytics.EFCore.Tests/Dfe.Analytics.EFCore.Tests.csproj index cf6059a..c68b4ec 100644 --- a/tests/Dfe.Analytics.EFCore.Tests/Dfe.Analytics.EFCore.Tests.csproj +++ b/tests/Dfe.Analytics.EFCore.Tests/Dfe.Analytics.EFCore.Tests.csproj @@ -16,8 +16,21 @@ + + + + + + + + + + + + +