Skip to content

Commit 2b9a763

Browse files
Merge pull request #15 from Stravaig-Projects/#10/named-connection-string
#10: Configure by named connection string
2 parents ae8351d + 16ae2b6 commit 2b9a763

File tree

11 files changed

+350
-20
lines changed

11 files changed

+350
-20
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,54 @@ A configuration provider that uses SQL Server as its backing store.
88

99
---
1010

11+
## Set up
12+
13+
The simplest way is to configure the SQL Server configuration from the existing configuration up to that point. Any configuration providers added after the call to `AddSqlServer` will not have their data alter the way that the SQL Server configuration provider is set up.
14+
15+
```csharp
16+
await Host.CreateDefaultBuilder(args)
17+
.ConfigureAppConfiguration(builder =>
18+
{
19+
builder.AddSqlServer(opts =>
20+
{
21+
// Will pull connection string, schema and table names from the
22+
// configuration system up to this point.
23+
opts.FromExistingConfiguration();
24+
});
25+
})
26+
```
27+
28+
with the corresponding information in `appsettings.json`, e.g.:
29+
30+
```json
31+
{
32+
"ConnectionStrings": {
33+
"ConfigDB": "*** Found in Secret Store ***"
34+
},
35+
"Stravaig": {
36+
"AppConfiguration": {
37+
"SchemaName": "Stravaig",
38+
"TableName": "AppConfiguration",
39+
"RefreshSeconds": 90,
40+
"ConnectionStringName": "ConfigDB"
41+
}
42+
}
43+
}
44+
```
45+
46+
The default configuration section used is `Stravaig.AppSettings`, however, this can be changed to what ever you prefer by passing in the path to the configuration section you prefer. e.g.
47+
48+
```csharp
49+
builder.AddSqlServer(opts =>
50+
{
51+
// Will pull connection string, schema and table names from the
52+
// configuration system at the specified config section.
53+
opts.FromExistingConfiguration("MyApp:SqlConfiguration");
54+
});
55+
```
56+
57+
58+
1159
## Contributing / Getting Started
1260

1361
* Ensure you have PowerShell 7.1.x or higher installed

release-notes/wip-release-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Date: ???
77
### Features
88

99
- #2: Create the initial provider
10+
- #10: Use a named connection string rather than supply it directly in the configuration source.
1011

1112
### Miscellaneous
1213

src/Example/appsettings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
{
2+
"ConnectionStrings": {
3+
"ConfigDB": "*** Found in User Secrets ***"
4+
},
25
"Stravaig": {
36
"AppConfiguration": {
47
"SchemaName": "Stravaig",
58
"TableName": "AppConfiguration",
69
"RefreshSeconds": 15,
7-
"ConnectionString": "*** Found in User Secrets ***"
10+
//"ConnectionString": "*** Found in User Secrets ***",
11+
"ConnectionStringName": "ConfigDB"
812
}
913
},
1014
"FeatureManager": {
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.Extensions.Configuration;
4+
using NUnit.Framework;
5+
using Shouldly;
6+
7+
namespace Stravaig.Configuration.SqlServer.Tests;
8+
9+
[TestFixture]
10+
public class SourceBuilderTests
11+
{
12+
[Test]
13+
public void DeveloperIsBeingObtuseByDeliberatelyNullingSchema_ThrowsException()
14+
{
15+
// Arrange
16+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
17+
var options = (SqlServerConfigurationOptions opts) =>
18+
{
19+
opts.ConnectionString = theConnectionString;
20+
opts.SchemaName = null!;
21+
};
22+
var configBuilder = SetupConfig();
23+
24+
// Act & Assert
25+
var ex = Should.Throw<SqlServerConfigurationProviderException>(
26+
() => SourceBuilder.BuildSource(configBuilder, options));
27+
28+
ex.Message.ShouldBe("The schema name is required to use SQL Server Configuration.");
29+
}
30+
31+
[Test]
32+
public void DeveloperIsBeingObtuseByDeliberatelyNullingTable_ThrowsException()
33+
{
34+
// Arrange
35+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
36+
var options = (SqlServerConfigurationOptions opts) =>
37+
{
38+
opts.ConnectionString = theConnectionString;
39+
opts.TableName = null!;
40+
};
41+
var configBuilder = SetupConfig();
42+
43+
// Act & Assert
44+
var ex = Should.Throw<SqlServerConfigurationProviderException>(
45+
() => SourceBuilder.BuildSource(configBuilder, options));
46+
47+
ex.Message.ShouldBe("The table name is required to use SQL Server Configuration.");
48+
}
49+
50+
[Test]
51+
public void SimplestOptions_HappyPath()
52+
{
53+
// Arrange
54+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
55+
var options = (SqlServerConfigurationOptions opts) =>
56+
{
57+
opts.ConnectionString = theConnectionString;
58+
};
59+
var configBuilder = SetupConfig();
60+
61+
// Act
62+
var source = SourceBuilder.BuildSource(configBuilder, options);
63+
64+
// Assert
65+
source.ConnectionString.ShouldBe(theConnectionString);
66+
source.RefreshInterval.ShouldBe(TimeSpan.FromSeconds(DefaultValues.NoRefresh));
67+
source.SchemaName.ShouldBe(DefaultValues.SchemaName);
68+
source.TableName.ShouldBe(DefaultValues.TableName);
69+
}
70+
71+
[Test]
72+
public void FillAllOptionsManually_HappyPath()
73+
{
74+
// Arrange
75+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
76+
const int refreshSeconds = 60;
77+
const string schemaName = "MyConfigInfo";
78+
const string tableName = "AppConfig";
79+
var options = (SqlServerConfigurationOptions opts) =>
80+
{
81+
opts.ConnectionString = theConnectionString;
82+
opts.RefreshSeconds = refreshSeconds;
83+
opts.SchemaName = schemaName;
84+
opts.TableName = tableName;
85+
};
86+
var configBuilder = SetupConfig();
87+
88+
// Act
89+
var source = SourceBuilder.BuildSource(configBuilder, options);
90+
91+
// Assert
92+
source.ConnectionString.ShouldBe(theConnectionString);
93+
source.RefreshInterval.ShouldBe(TimeSpan.FromSeconds(refreshSeconds));
94+
source.SchemaName.ShouldBe(schemaName);
95+
source.TableName.ShouldBe(tableName);
96+
}
97+
98+
[Test]
99+
public void NoConnectionString_ThrowsException()
100+
{
101+
// Arrange
102+
var configBuilder = SetupConfig();
103+
104+
// Act & Assert
105+
var ex = Should.Throw<SqlServerConfigurationProviderException>(
106+
() => SourceBuilder.BuildSource(configBuilder, null));
107+
ex.Message.ShouldBe("Cannot build a SQL Server Configuration Provider without a connection string.");
108+
}
109+
110+
[Test]
111+
public void FillAllOptionsFromExistingConfig_HappyPath()
112+
{
113+
// Arrange
114+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
115+
const int refreshSeconds = 60;
116+
const string schemaName = "MyConfigInfo";
117+
const string tableName = "AppConfig";
118+
var options = (SqlServerConfigurationOptions opts) =>
119+
{
120+
opts.ConfigurationSection = "SqlConfiguration";
121+
};
122+
var configBuilder = SetupConfig(builder =>
123+
{
124+
builder.AddInMemoryCollection(new[]
125+
{
126+
new KeyValuePair<string, string>("SqlConfiguration:ConnectionString", theConnectionString),
127+
new KeyValuePair<string, string>("SqlConfiguration:RefreshSeconds", refreshSeconds.ToString()),
128+
new KeyValuePair<string, string>("SqlConfiguration:SchemaName", schemaName),
129+
new KeyValuePair<string, string>("SqlConfiguration:TableName", tableName),
130+
});
131+
});
132+
133+
// Act
134+
var source = SourceBuilder.BuildSource(configBuilder, options);
135+
136+
// Assert
137+
source.ConnectionString.ShouldBe(theConnectionString);
138+
source.RefreshInterval.ShouldBe(TimeSpan.FromSeconds(refreshSeconds));
139+
source.SchemaName.ShouldBe(schemaName);
140+
source.TableName.ShouldBe(tableName);
141+
}
142+
143+
[Test]
144+
public void FillAllOptionsFromExistingConfigWithNamedConnectionString_HappyPath()
145+
{
146+
// Arrange
147+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
148+
const int refreshSeconds = 60;
149+
const string schemaName = "MyConfigInfo";
150+
const string tableName = "AppConfig";
151+
var options = (SqlServerConfigurationOptions opts) =>
152+
{
153+
opts.ConfigurationSection = "SqlConfiguration";
154+
};
155+
var configBuilder = SetupConfig(builder =>
156+
{
157+
builder.AddInMemoryCollection(new[]
158+
{
159+
new KeyValuePair<string, string>("ConnectionStrings:ConfigDB", theConnectionString),
160+
new KeyValuePair<string, string>("SqlConfiguration:RefreshSeconds", refreshSeconds.ToString()),
161+
new KeyValuePair<string, string>("SqlConfiguration:SchemaName", schemaName),
162+
new KeyValuePair<string, string>("SqlConfiguration:TableName", tableName),
163+
new KeyValuePair<string, string>("SqlConfiguration:ConnectionStringName", "ConfigDB")
164+
});
165+
});
166+
167+
// Act
168+
var source = SourceBuilder.BuildSource(configBuilder, options);
169+
170+
// Assert
171+
source.ConnectionString.ShouldBe(theConnectionString);
172+
source.RefreshInterval.ShouldBe(TimeSpan.FromSeconds(refreshSeconds));
173+
source.SchemaName.ShouldBe(schemaName);
174+
source.TableName.ShouldBe(tableName);
175+
}
176+
177+
[Test]
178+
public void TwoCompetingConnectionStrings_DirectOneWins()
179+
{
180+
// Arrange
181+
const string theConnectionString = "Server=MyServer;Database=MyDatabase";
182+
const string theLosingConnectionString = "Server=TheLosingServer;Database=TheLosingDatabase";
183+
const int refreshSeconds = 60;
184+
const string schemaName = "MyConfigInfo";
185+
const string tableName = "AppConfig";
186+
var options = (SqlServerConfigurationOptions opts) =>
187+
{
188+
opts.ConfigurationSection = "SqlConfiguration";
189+
};
190+
var configBuilder = SetupConfig(builder =>
191+
{
192+
builder.AddInMemoryCollection(new[]
193+
{
194+
new KeyValuePair<string, string>("ConnectionStrings:ConfigDB", theLosingConnectionString),
195+
new KeyValuePair<string, string>("SqlConfiguration:ConnectionString", theConnectionString),
196+
new KeyValuePair<string, string>("SqlConfiguration:RefreshSeconds", refreshSeconds.ToString()),
197+
new KeyValuePair<string, string>("SqlConfiguration:SchemaName", schemaName),
198+
new KeyValuePair<string, string>("SqlConfiguration:TableName", tableName),
199+
new KeyValuePair<string, string>("SqlConfiguration:ConnectionStringName", "ConfigDB")
200+
});
201+
});
202+
203+
// Act
204+
var source = SourceBuilder.BuildSource(configBuilder, options);
205+
206+
// Assert
207+
source.ConnectionString.ShouldBe(theConnectionString);
208+
source.RefreshInterval.ShouldBe(TimeSpan.FromSeconds(refreshSeconds));
209+
source.SchemaName.ShouldBe(schemaName);
210+
source.TableName.ShouldBe(tableName);
211+
}
212+
213+
private IConfigurationBuilder SetupConfig(Action<IConfigurationBuilder>? configure = null)
214+
{
215+
var builder = new ConfigurationBuilder();
216+
configure?.Invoke(builder);
217+
return builder;
218+
}
219+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nulling/@EntryIndexedValue">True</s:Boolean>
23
<s:Boolean x:Key="/Default/UserDictionary/Words/=Stravaig/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

src/Stravaig.Configuration.SqlServer/DefaultValues.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace Stravaig.Configuration.SqlServer;
22

33
public static class DefaultValues
44
{
5+
public const int NoRefresh = 0;
56
public const string SchemaName = "Stravaig";
67
public const string TableName = "AppConfiguration";
78
public const string ConfigurationSection = "Stravaig:AppConfiguration";
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Stravaig.Configuration.SqlServer;
5+
6+
internal static class SourceBuilder
7+
{
8+
public static SqlServerConfigurationSource BuildSource(IConfigurationBuilder builder, Action<SqlServerConfigurationOptions>? optionsBuilder)
9+
{
10+
var options = new SqlServerConfigurationOptions();
11+
optionsBuilder?.Invoke(options);
12+
13+
Lazy<IConfigurationRoot> configRoot = new (builder.Build);
14+
if (!string.IsNullOrWhiteSpace(options.ConfigurationSection))
15+
SetOptionsFromConfiguration(configRoot.Value, options);
16+
17+
// If a connection string has been supplied directly, it takes
18+
// precedent over a connection string reference.
19+
if (string.IsNullOrWhiteSpace(options.ConnectionString) &&
20+
!string.IsNullOrWhiteSpace(options.ConnectionStringName))
21+
ApplyFromConnectionStringsSection(options, configRoot.Value);
22+
23+
return new SqlServerConfigurationSource(
24+
options.ConnectionString ?? throw new SqlServerConfigurationProviderException("Cannot build a SQL Server Configuration Provider without a connection string."),
25+
TimeSpan.FromSeconds(options.RefreshSeconds),
26+
options.SchemaName ?? throw new SqlServerConfigurationProviderException("The schema name is required to use SQL Server Configuration."),
27+
options.TableName ?? throw new SqlServerConfigurationProviderException("The table name is required to use SQL Server Configuration."));
28+
}
29+
30+
private static void ApplyFromConnectionStringsSection(SqlServerConfigurationOptions options, IConfigurationRoot configRoot)
31+
{
32+
var connectionString = configRoot.GetConnectionString(options.ConnectionStringName);
33+
if (!string.IsNullOrWhiteSpace(connectionString))
34+
options.ConnectionString = connectionString;
35+
}
36+
37+
private static void SetOptionsFromConfiguration(IConfigurationRoot configRoot, SqlServerConfigurationOptions options)
38+
{
39+
var section = configRoot.GetSection(options.ConfigurationSection);
40+
section.Bind(options);
41+
}
42+
}

src/Stravaig.Configuration.SqlServer/SqlServerConfigurationBuilderExtensions.cs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,10 @@ namespace Stravaig.Configuration.SqlServer;
55

66
public static class SqlServerConfigurationBuilderExtensions
77
{
8-
public static IConfigurationBuilder AddSqlServer(this IConfigurationBuilder builder, Action<SqlServerConfigurationOptions>? optionsBuilder = null)
8+
public static IConfigurationBuilder AddSqlServer(this IConfigurationBuilder configBuilder, Action<SqlServerConfigurationOptions>? optionsBuilder = null)
99
{
10-
var options = new SqlServerConfigurationOptions();
11-
optionsBuilder?.Invoke(options);
12-
13-
if (!string.IsNullOrWhiteSpace(options.ConfigurationSection))
14-
SetOptionsFromConfiguration(builder.Build(), options);
10+
var options = SourceBuilder.BuildSource(configBuilder, optionsBuilder);
1511

16-
return builder.Add(new SqlServerConfigurationSource(
17-
options.ConnectionString ?? throw new InvalidOperationException("Cannot build a SQL Server Configuration Provider with a null connection string."),
18-
TimeSpan.FromSeconds(options.RefreshSeconds),
19-
options.SchemaName,
20-
options.TableName));
21-
}
22-
23-
private static void SetOptionsFromConfiguration(IConfigurationRoot configRoot, SqlServerConfigurationOptions options)
24-
{
25-
var section = configRoot.GetSection(options.ConfigurationSection);
26-
section.Bind(options);
12+
return configBuilder.Add(options);
2713
}
2814
}

0 commit comments

Comments
 (0)