diff --git a/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs b/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs new file mode 100644 index 0000000..fc76599 --- /dev/null +++ b/EstateReportingAPI.BusinessLogic/QueryTimingInterceptor.cs @@ -0,0 +1,98 @@ +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shared.EntityFramework; +using Shared.Logger; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.Text; +using Shared.General; + +namespace EstateReportingAPI.BusinessLogic; + +public class QueryTimingInterceptor : DbCommandInterceptor { + + internal void LogIfRequired(DbCommand command, + CommandExecutedEventData eventData) { + + Int32 threshold = ConfigurationReader.GetValueOrDefault("AppSettings", "EFQueryPerformanceThresholdMs", 500); + + if (eventData.Duration.TotalMilliseconds < threshold) + return; + Logger.LogWarning($"PERFORMANCE - EF Query took {eventData.Duration.TotalMilliseconds} ms\n{command.CommandText}\n"); + } + + public override DbDataReader ReaderExecuted(DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result) { + LogIfRequired(command, eventData); + return result; + } + + public override async ValueTask ReaderExecutedAsync(DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = new CancellationToken()) { + LogIfRequired(command, eventData); + + return result; + } +} + + +public class DbContextResolverX : IDbContextResolver where TContext : DbContext +{ + private readonly IServiceProvider _rootProvider; + private readonly IConfiguration _config; + private readonly DbCommandInterceptor Interceptor; + + public DbContextResolverX(IServiceProvider rootProvider, + IConfiguration config, + DbCommandInterceptor interceptor) + { + _rootProvider = rootProvider; + _config = config; + this.Interceptor = interceptor; + } + + public ResolvedDbContext Resolve(String connectionStringKey) + { + return this.Resolve(connectionStringKey, String.Empty); + } + + public ResolvedDbContext Resolve(String connectionStringKey, + String databaseNameSuffix) + { + IServiceScope scope = _rootProvider.CreateScope(); + String connectionString = _config.GetConnectionString(connectionStringKey); + if (String.IsNullOrWhiteSpace(connectionString)) + throw new InvalidOperationException($"Connection string for '{connectionStringKey}' not found."); + + // Update the connection string with the identifier if needed + if (!String.IsNullOrWhiteSpace(databaseNameSuffix)) + { + SqlConnectionStringBuilder builder = new(connectionString); + builder.InitialCatalog = $"{builder.InitialCatalog}-{databaseNameSuffix}"; + connectionString = builder.ConnectionString; + + + // Create an isolated service collection and provider + ServiceCollection services = new(); + services.AddDbContext(options => { + options.UseSqlServer(connectionString); + options.AddInterceptors(Interceptor); // attach here + }); + + ServiceProvider provider = services.BuildServiceProvider(); + scope = provider.CreateScope(); + // Standard resolution using DI container + } + + return new ResolvedDbContext(scope); + } +} diff --git a/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs b/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs index dc22aea..917f5e3 100644 --- a/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/RepositoryRegistry.cs @@ -1,4 +1,5 @@ -using TransactionProcessor.Database.Contexts; +using Microsoft.EntityFrameworkCore.Diagnostics; +using TransactionProcessor.Database.Contexts; namespace EstateReportingAPI.Bootstrapper; @@ -6,6 +7,7 @@ namespace EstateReportingAPI.Bootstrapper; using Common; using Lamar; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Shared.EntityFramework; using Shared.General; using System.Diagnostics.CodeAnalysis; @@ -18,16 +20,19 @@ public RepositoryRegistry(){ { this.AddSingleton(); } - - this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolver<>)); + this.AddSingleton(); + this.AddSingleton(typeof(IDbContextResolver<>), typeof(DbContextResolverX<>)); if (Startup.WebHostEnvironment.IsEnvironment("IntegrationTest") || Startup.Configuration.GetValue("ServiceOptions:UseInMemoryDatabase") == true) { this.AddDbContext(builder => builder.UseInMemoryDatabase("TransactionProcessorReadModel")); } - else - { - this.AddDbContext(options => - options.UseSqlServer(ConfigurationReader.GetConnectionString("TransactionProcessorReadModel"))); + else { + this.AddSingleton(); + this.AddDbContext((sp, options) => + { + options.UseSqlServer(ConfigurationReader.GetConnectionString("TransactionProcessorReadModel")); + options.AddInterceptors(sp.GetRequiredService()); + }); } } } \ No newline at end of file