Skip to content

Commit 5af93ce

Browse files
Merge pull request #108 from DFE-Digital/feature/appsettings
Database applications settings packagae
2 parents a2c565f + 1328a5e commit 5af93ce

24 files changed

Lines changed: 2816 additions & 2 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI & Pack GovUK.Dfe.CoreLibs.ApplicationSettings
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths:
7+
- "src/GovUK.Dfe.CoreLibs.ApplicationSettings/**"
8+
pull_request:
9+
branches: [ main ]
10+
paths:
11+
- "src/GovUK.Dfe.CoreLibs.ApplicationSettings/**"
12+
13+
jobs:
14+
build-and-test:
15+
uses: ./.github/workflows/build-test-template.yml
16+
with:
17+
project_name: GovUK.Dfe.CoreLibs.ApplicationSettings
18+
project_path: src/GovUK.Dfe.CoreLibs.ApplicationSettings
19+
secrets:
20+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
21+
22+
pack-and-release:
23+
needs: build-and-test
24+
if: needs.build-and-test.result == 'success'
25+
uses: ./.github/workflows/pack-template.yml
26+
with:
27+
project_name: GovUK.Dfe.CoreLibs.ApplicationSettings
28+
project_path: src/GovUK.Dfe.CoreLibs.ApplicationSettings/GovUK.Dfe.CoreLibs.ApplicationSettings.csproj
29+
secrets:
30+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}

GovUK.Dfe.CoreLibs.sln

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.10.35122.118
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.4.11605.240
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GovUK.Dfe.CoreLibs.Caching", "src\GovUK.Dfe.CoreLibs.Caching\GovUK.Dfe.CoreLibs.Caching.csproj", "{D88D58F0-18C4-4C3B-805B-8A483500E73E}"
77
EndProject
@@ -49,6 +49,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GovUK.Dfe.CoreLibs.Messagin
4949
EndProject
5050
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GovUK.Dfe.CoreLibs.Messaging.MassTransit.Tests", "src\Tests\GovUK.Dfe.CoreLibs.Messaging.MassTransit.Tests\GovUK.Dfe.CoreLibs.Messaging.MassTransit.Tests.csproj", "{1EA0F5A0-759C-4FF5-8C2C-CF4D4ACE252F}"
5151
EndProject
52+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GovUK.Dfe.CoreLibs.ApplicationSettings", "src\GovUK.Dfe.CoreLibs.ApplicationSettings\GovUK.Dfe.CoreLibs.ApplicationSettings.csproj", "{7EA4DA7D-AA0D-F78A-E03A-64C584868F78}"
53+
EndProject
54+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GovUK.Dfe.CoreLibs.ApplicationSettings.Tests", "src\Tests\GovUK.Dfe.CoreLibs.ApplicationSettings.Tests\GovUK.Dfe.CoreLibs.ApplicationSettings.Tests.csproj", "{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4}"
55+
EndProject
5256
Global
5357
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5458
Debug|Any CPU = Debug|Any CPU
@@ -142,6 +146,14 @@ Global
142146
{1EA0F5A0-759C-4FF5-8C2C-CF4D4ACE252F}.Debug|Any CPU.Build.0 = Debug|Any CPU
143147
{1EA0F5A0-759C-4FF5-8C2C-CF4D4ACE252F}.Release|Any CPU.ActiveCfg = Release|Any CPU
144148
{1EA0F5A0-759C-4FF5-8C2C-CF4D4ACE252F}.Release|Any CPU.Build.0 = Release|Any CPU
149+
{7EA4DA7D-AA0D-F78A-E03A-64C584868F78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
150+
{7EA4DA7D-AA0D-F78A-E03A-64C584868F78}.Debug|Any CPU.Build.0 = Debug|Any CPU
151+
{7EA4DA7D-AA0D-F78A-E03A-64C584868F78}.Release|Any CPU.ActiveCfg = Release|Any CPU
152+
{7EA4DA7D-AA0D-F78A-E03A-64C584868F78}.Release|Any CPU.Build.0 = Release|Any CPU
153+
{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
154+
{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
155+
{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
156+
{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4}.Release|Any CPU.Build.0 = Release|Any CPU
145157
EndGlobalSection
146158
GlobalSection(SolutionProperties) = preSolution
147159
HideSolutionNode = FALSE
@@ -156,6 +168,7 @@ Global
156168
{8D3F0E0F-9C7F-6B1F-BD5C-3E4F5A6B7C8D} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
157169
{1F2A3B4C-5D6E-7F8A-9B0C-1D2E3F4A5B6C} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
158170
{1EA0F5A0-759C-4FF5-8C2C-CF4D4ACE252F} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
171+
{EA11C968-CDF4-6F99-F7B2-8DB18B5793F4} = {3F89DCAD-8EC7-41ED-A08F-A9EFAE263EB4}
159172
EndGlobalSection
160173
GlobalSection(ExtensibilityGlobals) = postSolution
161174
SolutionGuid = {01D11FBC-6C66-43E4-8F1F-46B105EDD95C}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace GovUK.Dfe.CoreLibs.ApplicationSettings.Configuration;
2+
3+
public class ApplicationSettingsOptions
4+
{
5+
public const string ConfigurationSection = "ApplicationSettings";
6+
7+
/// <summary>
8+
/// Database schema name for the ApplicationSettings table
9+
/// </summary>
10+
public string? Schema { get; set; } = null;
11+
12+
/// <summary>
13+
/// Enable caching of settings in memory
14+
/// </summary>
15+
public bool EnableCaching { get; set; } = true;
16+
17+
/// <summary>
18+
/// Cache expiration time in minutes
19+
/// </summary>
20+
public int CacheExpirationMinutes { get; set; } = 30;
21+
22+
/// <summary>
23+
/// Default category for settings without specified category
24+
/// </summary>
25+
public string DefaultCategory { get; set; } = "General";
26+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Configuration;
2+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Entities;
3+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Extensions;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.Options;
6+
7+
namespace GovUK.Dfe.CoreLibs.ApplicationSettings.Data;
8+
9+
public class ApplicationSettingsDbContext : DbContext
10+
{
11+
private readonly ApplicationSettingsOptions _options;
12+
13+
public ApplicationSettingsDbContext(DbContextOptions<ApplicationSettingsDbContext> options, IOptions<ApplicationSettingsOptions> settingsOptions)
14+
: base(options)
15+
{
16+
_options = settingsOptions.Value;
17+
}
18+
19+
public DbSet<ApplicationSetting> ApplicationSettings { get; set; } = null!;
20+
21+
protected override void OnModelCreating(ModelBuilder modelBuilder)
22+
{
23+
// Use the extension method for configuration
24+
modelBuilder.ConfigureApplicationSettings(_options);
25+
26+
base.OnModelCreating(modelBuilder);
27+
}
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace GovUK.Dfe.CoreLibs.ApplicationSettings.Entities;
2+
3+
public class ApplicationSetting
4+
{
5+
public int Id { get; set; }
6+
public string Key { get; set; } = string.Empty;
7+
public string Value { get; set; } = string.Empty;
8+
public string? Description { get; set; }
9+
public string Category { get; set; } = "General";
10+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
11+
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
12+
public string? CreatedBy { get; set; }
13+
public string? UpdatedBy { get; set; }
14+
public bool IsActive { get; set; } = true;
15+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Configuration;
2+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Entities;
3+
using Microsoft.EntityFrameworkCore;
4+
5+
namespace GovUK.Dfe.CoreLibs.ApplicationSettings.Extensions;
6+
7+
public static class DbContextExtensions
8+
{
9+
/// <summary>
10+
/// Adds ApplicationSettings entity configuration to an existing DbContext
11+
/// </summary>
12+
/// <param name="modelBuilder">The ModelBuilder instance</param>
13+
/// <param name="schema">Optional schema name. If null, uses default schema</param>
14+
/// <param name="tableName">Optional table name. Defaults to "ApplicationSettings"</param>
15+
public static void ConfigureApplicationSettings(
16+
this ModelBuilder modelBuilder,
17+
string? schema = null,
18+
string tableName = "ApplicationSettings")
19+
{
20+
// Apply default schema if specified
21+
if (!string.IsNullOrEmpty(schema))
22+
{
23+
modelBuilder.HasDefaultSchema(schema);
24+
}
25+
26+
modelBuilder.Entity<ApplicationSetting>(entity =>
27+
{
28+
// Configure table with optional schema
29+
if (!string.IsNullOrEmpty(schema))
30+
{
31+
entity.ToTable(tableName, schema);
32+
}
33+
else
34+
{
35+
entity.ToTable(tableName);
36+
}
37+
38+
entity.HasKey(e => e.Id);
39+
40+
entity.Property(e => e.Key)
41+
.IsRequired()
42+
.HasMaxLength(255);
43+
44+
entity.Property(e => e.Value)
45+
.IsRequired();
46+
47+
entity.Property(e => e.Description)
48+
.HasMaxLength(500);
49+
50+
entity.Property(e => e.Category)
51+
.IsRequired()
52+
.HasMaxLength(100)
53+
.HasDefaultValue("General");
54+
55+
entity.Property(e => e.CreatedBy)
56+
.HasMaxLength(255);
57+
58+
entity.Property(e => e.UpdatedBy)
59+
.HasMaxLength(255);
60+
61+
// Create unique index on Key for fast lookups
62+
entity.HasIndex(e => e.Key)
63+
.IsUnique()
64+
.HasDatabaseName("IX_ApplicationSettings_Key");
65+
66+
// Create index on Category for filtered queries
67+
entity.HasIndex(e => e.Category)
68+
.HasDatabaseName("IX_ApplicationSettings_Category");
69+
});
70+
}
71+
72+
/// <summary>
73+
/// Adds ApplicationSettings entity configuration using options from DI
74+
/// </summary>
75+
/// <param name="modelBuilder">The ModelBuilder instance</param>
76+
/// <param name="options">ApplicationSettings options</param>
77+
public static void ConfigureApplicationSettings(
78+
this ModelBuilder modelBuilder,
79+
ApplicationSettingsOptions options)
80+
{
81+
modelBuilder.ConfigureApplicationSettings(options.Schema, "ApplicationSettings");
82+
}
83+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Configuration;
2+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Data;
3+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Interfaces;
4+
using GovUK.Dfe.CoreLibs.ApplicationSettings.Services;
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace GovUK.Dfe.CoreLibs.ApplicationSettings.Extensions;
10+
11+
public static class ServiceCollectionExtensions
12+
{
13+
// Group all AddApplicationSettings overloads together
14+
public static IServiceCollection AddApplicationSettings(
15+
this IServiceCollection services,
16+
IConfiguration configuration,
17+
string connectionStringName = "DefaultConnection",
18+
string? schema = null)
19+
{
20+
// Configure options using shared method
21+
services.ConfigureApplicationSettingsOptions(configuration, schema);
22+
23+
// Add DbContext
24+
services.AddDbContext<ApplicationSettingsDbContext>(options =>
25+
options.UseSqlServer(configuration.GetConnectionString(connectionStringName)));
26+
27+
// Add shared dependencies and service
28+
return services.AddApplicationSettingsCore<ApplicationSettingsService>();
29+
}
30+
31+
public static IServiceCollection AddApplicationSettings(
32+
this IServiceCollection services,
33+
string connectionString,
34+
Action<ApplicationSettingsOptions>? configureOptions = null)
35+
{
36+
// Configure options with defaults and optional customization
37+
services.Configure<ApplicationSettingsOptions>(options =>
38+
{
39+
// Set defaults using shared method
40+
SetDefaultOptions(options);
41+
42+
// Apply custom configuration if provided
43+
configureOptions?.Invoke(options);
44+
});
45+
46+
// Add DbContext
47+
services.AddDbContext<ApplicationSettingsDbContext>(options =>
48+
options.UseSqlServer(connectionString));
49+
50+
// Add shared dependencies and service
51+
return services.AddApplicationSettingsCore<ApplicationSettingsService>();
52+
}
53+
54+
// Now place the different method after all AddApplicationSettings overloads
55+
/// <summary>
56+
/// Adds ApplicationSettings service using an existing DbContext
57+
/// </summary>
58+
/// <typeparam name="TContext">The existing DbContext type</typeparam>
59+
/// <param name="services">Service collection</param>
60+
/// <param name="configuration">Configuration</param>
61+
/// <param name="schema">Optional schema override</param>
62+
/// <returns>Service collection</returns>
63+
public static IServiceCollection AddApplicationSettingsWithExistingContext<TContext>(
64+
this IServiceCollection services,
65+
IConfiguration configuration,
66+
string? schema = null)
67+
where TContext : DbContext
68+
{
69+
// Configure options using shared method
70+
services.ConfigureApplicationSettingsOptions(configuration, schema);
71+
72+
// Add shared dependencies and service
73+
return services.AddApplicationSettingsCore<ExistingContextApplicationSettingsService<TContext>>();
74+
}
75+
76+
// All private helper methods remain the same...
77+
/// <summary>
78+
/// Configures ApplicationSettingsOptions with defaults and reads from configuration
79+
/// </summary>
80+
/// <param name="services">Service collection</param>
81+
/// <param name="configuration">Configuration</param>
82+
/// <param name="schema">Optional schema override</param>
83+
private static void ConfigureApplicationSettingsOptions(
84+
this IServiceCollection services,
85+
IConfiguration configuration,
86+
string? schema = null)
87+
{
88+
services.Configure<ApplicationSettingsOptions>(options =>
89+
{
90+
// Set defaults
91+
SetDefaultOptions(options, schema);
92+
93+
// Read from configuration section if it exists
94+
var section = configuration.GetSection(ApplicationSettingsOptions.ConfigurationSection);
95+
if (section.Exists())
96+
{
97+
ApplyConfigurationSettings(options, section, schema);
98+
}
99+
});
100+
}
101+
102+
/// <summary>
103+
/// Sets default values for ApplicationSettingsOptions
104+
/// </summary>
105+
/// <param name="options">Options to configure</param>
106+
/// <param name="schema">Optional schema override</param>
107+
private static void SetDefaultOptions(ApplicationSettingsOptions options, string? schema = null)
108+
{
109+
options.EnableCaching = true;
110+
options.CacheExpirationMinutes = 30;
111+
options.DefaultCategory = "General";
112+
options.Schema = schema;
113+
}
114+
115+
/// <summary>
116+
/// Applies configuration settings from IConfiguration section
117+
/// </summary>
118+
/// <param name="options">Options to configure</param>
119+
/// <param name="section">Configuration section</param>
120+
/// <param name="schema">Optional schema override</param>
121+
private static void ApplyConfigurationSettings(
122+
ApplicationSettingsOptions options,
123+
IConfiguration section,
124+
string? schema = null)
125+
{
126+
if (bool.TryParse(section["EnableCaching"], out bool enableCaching))
127+
options.EnableCaching = enableCaching;
128+
129+
if (int.TryParse(section["CacheExpirationMinutes"], out int cacheExpiration))
130+
options.CacheExpirationMinutes = cacheExpiration;
131+
132+
if (!string.IsNullOrEmpty(section["DefaultCategory"]))
133+
options.DefaultCategory = section["DefaultCategory"] ?? string.Empty;
134+
135+
// Schema from configuration (only if not overridden by parameter)
136+
if (schema == null && !string.IsNullOrEmpty(section["Schema"]))
137+
options.Schema = section["Schema"];
138+
}
139+
140+
/// <summary>
141+
/// Adds core dependencies and service registration
142+
/// </summary>
143+
/// <typeparam name="TService">Service implementation type</typeparam>
144+
/// <param name="services">Service collection</param>
145+
/// <returns>Service collection</returns>
146+
private static IServiceCollection AddApplicationSettingsCore<TService>(this IServiceCollection services)
147+
where TService : class, IApplicationSettingsService
148+
{
149+
// Add memory cache if not already added
150+
services.AddMemoryCache();
151+
152+
// Add the service
153+
services.AddScoped<IApplicationSettingsService, TService>();
154+
155+
return services;
156+
}
157+
}

0 commit comments

Comments
 (0)