Skip to content
78 changes: 39 additions & 39 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.GitHub" Version="10.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="FsCheck" Version="3.3.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.10" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.4" />
</ItemGroup>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.GitHub" Version="10.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="FsCheck" Version="3.3.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.Google" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.301" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.10" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.4" />
</ItemGroup>
</Project>
68 changes: 68 additions & 0 deletions docs/articles/ef-core-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# EF Core Diagnostics

NetCoreApplicationTemplate keeps optional EF Core diagnostics disabled by default so normal application logging remains efficient and production-oriented.

EF Core already integrates with `Microsoft.Extensions.Logging`. Normal EF Core categories such as `Microsoft.EntityFrameworkCore.Database.Command` continue to follow the application's configured logging levels without enabling any additional diagnostics in the data-access layer.

## Configuration

Optional diagnostics are configured beneath `ProjectTemplate:DataAccess:Diagnostics`:

```json
{
"ProjectTemplate": {
"DataAccess": {
"Diagnostics": {
"EnableDetailedErrors": false,
"EnableEfCoreTraceBridge": false
}
}
}
}
```

Both settings default to `false` when omitted.

| Setting | Default | Purpose |
|:--|:--:|:--|
| `EnableDetailedErrors` | `false` | Calls EF Core `EnableDetailedErrors()` for the configured `ApplicationDbContext`. This can improve property-level exception diagnostics but adds diagnostic work during query materialization. |
| `EnableEfCoreTraceBridge` | `false` | Enables EF Core simple logging through `LogTo(...)` and forwards those messages through the application `ILogger<ApplicationDbContext>` at `Trace` level using event ID `19000`. |

The settings apply consistently to both scoped `ApplicationDbContext` instances and contexts created through `IDbContextFactory<ApplicationDbContext>`.

## Recommended Operational Use

Leave both settings disabled during normal operation and use standard logging configuration first. For example, EF Core category verbosity can be changed with the application's normal logging provider configuration without enabling the custom trace bridge.

Enable one of the diagnostic settings temporarily when investigating a specific data-access problem and disable it after the investigation. Configuration can be supplied through the normal ASP.NET Core configuration providers, including environment-specific JSON, environment variables, user secrets, or deployment configuration.

Environment variables use the normal double-underscore mapping. For example:

```text
ProjectTemplate__DataAccess__Diagnostics__EnableDetailedErrors=true
ProjectTemplate__DataAccess__Diagnostics__EnableEfCoreTraceBridge=true
```

`EnableEfCoreTraceBridge` is intentionally independent from the hosting environment. This allows an operator to turn on temporary diagnostics in a controlled production deployment without rebuilding the application or pretending that the deployment is a Development environment.

## Detailed Errors vs. Sensitive-Data Logging

`EnableDetailedErrors()` and `EnableSensitiveDataLogging()` are separate EF Core features.

Enabling `ProjectTemplate:DataAccess:Diagnostics:EnableDetailedErrors` does **not** enable sensitive-data logging. The template does not enable `EnableSensitiveDataLogging()` through these settings, and sensitive-data logging remains disabled unless a consuming application explicitly introduces and configures that behavior itself.

This distinction matters in production because sensitive-data logging can expose application values in diagnostic output. Do not treat the detailed-errors option as permission to log confidential, regulated, credential, token, or personally identifiable values.

## Trace Bridge vs. Standard EF Core Logging

The optional trace bridge exists for cases where an operator specifically wants EF Core's simple `LogTo(...)` stream routed through the application's `ILogger<ApplicationDbContext>` category.

It is disabled by default because standard EF Core logging already flows through `Microsoft.Extensions.Logging`, while a second `LogTo(...)` pipeline can cause EF Core to format additional diagnostic messages and can duplicate information already available through normal EF Core categories.

Prefer normal logging-category configuration for ongoing observability. Treat the trace bridge as a temporary troubleshooting tool.

## Save-Changes Interceptor

The diagnostics settings do not control the template's `ApplicationSaveChangesInterceptor`.

When EF Core data access is enabled, the save-changes interceptor remains registered and attached regardless of whether detailed errors or the trace bridge are enabled. Disabling optional diagnostics therefore does not remove auditing, canonicalization, mutation-manifest, or other save-pipeline behavior owned by the interceptor and save-changes pipeline.
2 changes: 2 additions & 0 deletions docs/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
items:
- name: Data Access
href: data-access.md
- name: EF Core Diagnostics
href: ef-core-diagnostics.md
- name: EF Core Save Pipeline
href: ef-core-save-pipeline.md
- name: Audit Accountability Integration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,6 @@ public sealed partial class ApplicationDbContext(
/// </summary>
public DbSet<ExternalLoginAccount> ExternalLoginAccounts => Set<ExternalLoginAccount>();

[LoggerMessage(
EventId = 19000,
Level = LogLevel.Trace,
Message = "{EfCoreMessage}")]
private static partial void LogEfCoreMessage(
ILogger logger,
string efCoreMessage);

[LoggerMessage(
EventId = 19001,
Level = LogLevel.Warning,
Expand Down Expand Up @@ -74,10 +66,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
ApplicationSaveChangesInterceptor interceptor =
_configuredSaveChangesInterceptor ?? new ApplicationSaveChangesInterceptor(_saveChangesPipeline);

_ = optionsBuilder
.LogTo(message => LogEfCoreMessage(_logger, message), LogLevel.Trace)
.AddInterceptors(interceptor)
.EnableDetailedErrors();
_ = optionsBuilder.AddInterceptors(interceptor);

base.OnConfiguring(optionsBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using ProjectTemplate.Infrastructure.Data.Auditing;
using ProjectTemplate.Infrastructure.Data.ExternalLogins;
using ProjectTemplate.Infrastructure.Data.Options;
Expand Down Expand Up @@ -31,6 +32,8 @@ public static IServiceCollection AddApplicationInfrastructureDataAccess(
.Validate(options => DataAccessOptions.IsDisabledProvider(options.Provider)
|| !string.IsNullOrWhiteSpace(options.ConnectionStringName),
"ProjectTemplate:DataAccess:ConnectionStringName must not be empty when data access is enabled.")
.Validate(options => options.Diagnostics is not null,
"ProjectTemplate:DataAccess:Diagnostics must be configured as an object when specified.")
.Validate(options => AuditStorageModes.IsSupported(options.Auditing.StorageMode),
"ProjectTemplate:DataAccess:Auditing:StorageMode must be Local, Outbox, or ExternalSink.")
.ValidateOnStart();
Expand Down Expand Up @@ -61,16 +64,17 @@ public static IServiceCollection AddApplicationInfrastructureDataAccess(
services.TryAddScoped<IApplicationMutationAuditReceiptAccessor, ApplicationDbContextMutationAuditReceiptAccessor>();
services.TryAddScoped<ApplicationSaveChangesInterceptor>();

services.AddDbContext<ApplicationDbContext>(options => ConfigureProvider(
services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
ConfigureDataAccess(
serviceProvider,
options,
registration.Provider,
registration.ConnectionString));
registration));

services.AddDbContextFactory<ApplicationDbContext>(
options => ConfigureProvider(
options,
registration.Provider,
registration.ConnectionString),
(serviceProvider, options) => ConfigureDataAccess(
serviceProvider,
options,
registration),
ServiceLifetime.Scoped);

services.TryAddScoped<IExternalLoginAccountResolver, EfCoreExternalLoginAccountResolver>();
Expand Down Expand Up @@ -110,7 +114,38 @@ private static DataAccessRegistration ResolveDataAccessRegistration(IConfigurati
string connectionString = configuration.GetConnectionString(connectionStringName)
?? throw new InvalidOperationException($"Connection string '{connectionStringName}' was not configured.");

return DataAccessRegistration.Enabled(provider, connectionString, auditStorageMode);
return DataAccessRegistration.Enabled(
provider,
connectionString,
auditStorageMode,
dataAccessOptions.Diagnostics.EnableDetailedErrors,
dataAccessOptions.Diagnostics.EnableEfCoreTraceBridge);
}

private static void ConfigureDataAccess(
IServiceProvider serviceProvider,
DbContextOptionsBuilder options,
DataAccessRegistration registration)
{
ConfigureProvider(
options,
registration.Provider,
registration.ConnectionString);

if (registration.EnableDetailedErrors)
{
_ = options.EnableDetailedErrors();
}

if (registration.EnableEfCoreTraceBridge)
{
ILogger<ApplicationDbContext> logger = serviceProvider
.GetRequiredService<ILogger<ApplicationDbContext>>();

_ = options.LogTo(
message => EfCoreDiagnosticsLogging.LogEfCoreMessage(logger, message),
LogLevel.Trace);
}
}

private static void ConfigureProvider(
Expand Down Expand Up @@ -138,21 +173,48 @@ private readonly record struct DataAccessRegistration(
string Provider,
string ConnectionString,
string AuditStorageMode,
bool EnableDetailedErrors,
bool EnableEfCoreTraceBridge,
bool IsDisabled)
{
public static DataAccessRegistration Enabled(
string provider,
string connectionString,
string auditStorageMode)
string auditStorageMode,
bool enableDetailedErrors,
bool enableEfCoreTraceBridge)
{
return new(provider, connectionString, auditStorageMode, false);
return new(
provider,
connectionString,
auditStorageMode,
enableDetailedErrors,
enableEfCoreTraceBridge,
false);
}

public static DataAccessRegistration Disabled(
string provider,
string auditStorageMode)
{
return new(provider, string.Empty, auditStorageMode, true);
return new(
provider,
string.Empty,
auditStorageMode,
false,
false,
true);
}
}
}

internal static partial class EfCoreDiagnosticsLogging
{
[LoggerMessage(
EventId = 19000,
Level = LogLevel.Trace,
Message = "{EfCoreMessage}")]
internal static partial void LogEfCoreMessage(
ILogger logger,
string efCoreMessage);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public sealed class DataAccessOptions

public string ConnectionStringName { get; init; } = "ApplicationDatabase";

public DataDiagnosticsOptions Diagnostics { get; init; } = new();

public DataAuditingOptions Auditing { get; init; } = new();

public static bool IsDisabledProvider(string? provider)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace ProjectTemplate.Infrastructure.Data.Options;

/// <summary>
/// Controls optional EF Core diagnostics for application data access.
/// </summary>
public sealed class DataDiagnosticsOptions
{
/// <summary>
/// Gets a value indicating whether EF Core detailed errors are enabled.
/// </summary>
public bool EnableDetailedErrors { get; init; }

/// <summary>
/// Gets a value indicating whether EF Core simple logging is bridged into the application logger at Trace level.
/// </summary>
public bool EnableEfCoreTraceBridge { get; init; }
}
Loading