diff --git a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs index b127b9b42d..8486fe3db2 100644 --- a/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/PlatformReportsDataService.cs @@ -1,10 +1,9 @@ namespace DigitalLearningSolutions.Data.DataServices { using Dapper; + using DigitalLearningSolutions.Data.Factories; using DigitalLearningSolutions.Data.Models.PlatformReports; - using DigitalLearningSolutions.Data.Models.SelfAssessments; using DigitalLearningSolutions.Data.Models.TrackingSystem; - using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Data; @@ -37,9 +36,9 @@ IEnumerable GetFilteredCourseActivity( ); DateTime? GetStartOfCourseActivity(); } - public class PlatformReportsDataService : IPlatformReportsDataService + public class PlatformReportsDataService(IReadOnlyDbConnectionFactory factory) : IPlatformReportsDataService { - private readonly IDbConnection connection; + private readonly IDbConnection connection = factory.CreateConnection(); private readonly string selectSelfAssessmentActivity = @"SELECT Cast(al.ActivityDate As Date) As ActivityDate, SUM(CAST(al.Enrolled AS Int)) AS Enrolled, SUM(CAST((al.Submitted | al.SignedOff) AS Int)) AS Completed FROM ReportSelfAssessmentActivityLog AS al WITH (NOLOCK) INNER JOIN Centres AS ce WITH (NOLOCK) ON al.CentreID = ce.CentreID INNER JOIN @@ -60,10 +59,6 @@ private string GetSelfAssessmentWhereClause(bool supervised) return supervised ? " (sa.SupervisorResultsReview = 1 OR SupervisorSelfAssessmentReview = 1)" : " (sa.SupervisorResultsReview = 0 AND SupervisorSelfAssessmentReview = 0)"; } - public PlatformReportsDataService(IDbConnection connection) - { - this.connection = connection; - } public PlatformUsageSummary GetPlatformUsageSummary() { return connection.QueryFirstOrDefault( diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs index 744580c411..7312ed00c9 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/DCSAReportDataService.cs @@ -1,6 +1,7 @@ namespace DigitalLearningSolutions.Data.DataServices.SelfAssessmentDataService { using Dapper; + using DigitalLearningSolutions.Data.Factories; using DigitalLearningSolutions.Data.Models.SelfAssessments.Export; using Microsoft.Extensions.Logging; using System.Collections.Generic; @@ -11,16 +12,10 @@ public interface IDCSAReportDataService IEnumerable GetDelegateCompletionStatusForCentre(int centreId); IEnumerable GetOutcomeSummaryForCentre(int centreId); } - public partial class DCSAReportDataService : IDCSAReportDataService + public partial class DCSAReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : IDCSAReportDataService { - private readonly IDbConnection connection; - private readonly ILogger logger; - - public DCSAReportDataService(IDbConnection connection, ILogger logger) - { - this.connection = connection; - this.logger = logger; - } + private readonly IDbConnection connection = factory.CreateConnection(); + private readonly ILogger logger = logger; public IEnumerable GetDelegateCompletionStatusForCentre(int centreId) { diff --git a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs index c46ffd6a39..3a593d515c 100644 --- a/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/SelfAssessmentDataService/SelfAsssessmentReportDataService.cs @@ -6,23 +6,17 @@ using Microsoft.Extensions.Logging; using System.Collections.Generic; using System.Data; - using ClosedXML.Excel; + using DigitalLearningSolutions.Data.Factories; public interface ISelfAssessmentReportDataService { IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId); IEnumerable GetSelfAssessmentReportDataForCentre(int centreId, int selfAssessmentId); } - public partial class SelfAssessmentReportDataService : ISelfAssessmentReportDataService + public partial class SelfAssessmentReportDataService(IReadOnlyDbConnectionFactory factory, ILogger logger) : ISelfAssessmentReportDataService { - private readonly IDbConnection connection; - private readonly ILogger logger; - - public SelfAssessmentReportDataService(IDbConnection connection, ILogger logger) - { - this.connection = connection; - this.logger = logger; - } + private readonly IDbConnection connection = factory.CreateConnection(); + private readonly ILogger logger = logger; public IEnumerable GetSelfAssessmentsForReportList(int centreId, int? categoryId) { diff --git a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj index 27ae4ae17e..9f8678de17 100644 --- a/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj +++ b/DigitalLearningSolutions.Data/DigitalLearningSolutions.Data.csproj @@ -17,6 +17,7 @@ + diff --git a/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs new file mode 100644 index 0000000000..0015ca8775 --- /dev/null +++ b/DigitalLearningSolutions.Data/Factories/ReadOnlyConnectionFactory.cs @@ -0,0 +1,29 @@ +namespace DigitalLearningSolutions.Data.Factories +{ + using Microsoft.Data.SqlClient; + using Microsoft.Extensions.Configuration; + using System; + using System.Data; + public interface IReadOnlyDbConnectionFactory + { + IDbConnection CreateConnection(); + } + public class ReadOnlyDbConnectionFactory : IReadOnlyDbConnectionFactory + { + private const string ReadOnlyConnectionName = "ReadOnlyConnection"; + private readonly string connectionString; + + public ReadOnlyDbConnectionFactory(IConfiguration config) + { + // Ensure the connection string is not null to avoid CS8601 + connectionString = config.GetConnectionString(ReadOnlyConnectionName) + ?? throw new InvalidOperationException($"Connection string '{ReadOnlyConnectionName}' is not configured."); + } + + public IDbConnection CreateConnection() + { + // Ensure the connection is not enlisted in the trasaction scope to avoid distributed transaction errors: + return new SqlConnection(connectionString + ";Enlist=false"); + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs index bb691af8ad..1790e61a09 100644 --- a/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs +++ b/DigitalLearningSolutions.Web/Helpers/ConfigHelper.cs @@ -8,6 +8,7 @@ public static class ConfigHelper { public const string DefaultConnectionStringName = "DefaultConnection"; + public const string ReadOnlyConnectionStringName = "ReadOnlyConnection"; public const string UnitTestConnectionStringName = "UnitTestConnection"; public static IConfigurationRoot GetAppConfig() diff --git a/DigitalLearningSolutions.Web/Startup.cs b/DigitalLearningSolutions.Web/Startup.cs index 07b0c84237..bf86444202 100644 --- a/DigitalLearningSolutions.Web/Startup.cs +++ b/DigitalLearningSolutions.Web/Startup.cs @@ -1,13 +1,5 @@ namespace DigitalLearningSolutions.Web { - using System.Collections.Generic; - using System.Data; - using System.IO; - using System.Linq; - using System.Security.Claims; - using System.Threading.Tasks; - using System.Transactions; - using System.Web; using AspNetCoreRateLimit; using DigitalLearningSolutions.Data.ApiClients; using DigitalLearningSolutions.Data.DataServices; @@ -39,6 +31,7 @@ namespace DigitalLearningSolutions.Web using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.HttpOverrides; + using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; @@ -46,12 +39,19 @@ namespace DigitalLearningSolutions.Web using Microsoft.Extensions.Hosting; using Microsoft.FeatureManagement; using Microsoft.IdentityModel.Protocols.OpenIdConnect; - using Microsoft.AspNetCore.Identity; + using Serilog; + using System; + using System.Collections.Generic; + using System.Data; + using System.IO; + using System.Linq; + using System.Security.Claims; + using System.Threading.Tasks; + using System.Transactions; + using System.Web; using static DigitalLearningSolutions.Data.DataServices.ICentreApplicationsDataService; using static DigitalLearningSolutions.Web.Services.ICentreApplicationsService; using static DigitalLearningSolutions.Web.Services.ICentreSelfAssessmentsService; - using System; - using Serilog; public class Startup { @@ -65,7 +65,6 @@ public Startup(IConfiguration config, IHostEnvironment env) this.config = config; this.env = env; } - public void ConfigureServices(IServiceCollection services) { ConfigureIpRateLimiting(services); @@ -194,6 +193,8 @@ public void ConfigureServices(IServiceCollection services) // Register database connection for Dapper. services.AddScoped(_ => new SqlConnection(defaultConnectionString)); + // Register factory for read-only replica connections + services.AddScoped(); Dapper.SqlMapper.Settings.CommandTimeout = 60; MultiPageFormService.InitConnection(new SqlConnection(defaultConnectionString)); @@ -510,6 +511,7 @@ private static void RegisterDataServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -525,7 +527,6 @@ private static void RegisterDataServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/DigitalLearningSolutions.Web/appsettings.json b/DigitalLearningSolutions.Web/appsettings.json index 4d7000b3e3..701341469d 100644 --- a/DigitalLearningSolutions.Web/appsettings.json +++ b/DigitalLearningSolutions.Web/appsettings.json @@ -2,6 +2,7 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Data Source=localhost;Initial Catalog=mbdbx101_uar;Integrated Security=True;encrypt=false;TrustServerCertificate=true;", + "ReadOnlyConnection": "Data Source=localhost;Initial Catalog=mbdbx101_uar;Integrated Security=True;encrypt=false;TrustServerCertificate=true;", "UnitTestConnection": "Data Source=localhost;Initial Catalog=mbdbx101_uar_test;Integrated Security=True;encrypt=false;TrustServerCertificate=true;" }, "CurrentSystemBaseUrl": "https://www.dls.nhs.uk",