Skip to content

Commit 39afa3f

Browse files
ADded documentation
1 parent 4bafc3d commit 39afa3f

20 files changed

Lines changed: 767 additions & 94 deletions

README.md

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,190 @@
11
# ConfigurationProvider backed by Sql Stream Store [![Build Status](https://travis-ci.org/Erwinvandervalk/Config.SqlStreamStore.svg?branch=master)](https://travis-ci.org/Erwinvandervalk/Config.SqlStreamStore)
22

3-
## Example use cases
3+
This library allows you to store configuration settings (key-value pairs) in a SQl Stream Store stream.
4+
5+
This allows the following scenario's:
6+
7+
1. All application instances in a webfarm can share the same configuration settings.
8+
1. The settings are audited, where the last x (defualt == 10) versions are stored. It's possible to revert back to a previous version of the settings.
9+
1. If multiple instances attempt to write the same setting, the writing is idempotent.
10+
1. The application can be notified of changes in the settings.
11+
1. The settings can be encrypted 'at rest'. (The actual encryption is up to the consumer)
12+
13+
This functionality is somewhat similar to using a key value store like Consul. If you are not yet ready to embrace consul, but do require a centralized key value store, then this might be the library for you, especially if you already use SQL Stream Store.
414

515
## Getting started
616

17+
### Dependencies
18+
19+
To get started, add a reference to Config.StreamStore and to your stream store implementation of choice, such as SqlStreamStore.MsSql.
20+
21+
### Building
22+
23+
To build the solution, execute the build.cmd / build.sh
24+
25+
### Registering SQL Stream store
26+
Then you register your configuration source like this:
27+
28+
``` c#
29+
30+
/*
31+
This example adds SQL Stream Store as the provider with the lowest priority.
32+
This means you can override the values in SQL Stream Store with settings defined
33+
on the application server. (recommended)
34+
35+
*/
36+
37+
// IN this example, the connection string is read from a configuration setting called 'ConnectionString'. This
38+
// has to be defined either as a value in the ini file, as a command line setting or as an environment variable.
39+
40+
var config = new ConfigurationBuilder()
41+
// Get the connection string from the config and build a new SSS implementation
42+
.AddStreamStore((c) => new MsSqlStreamStore(new MsSqlStreamStoreSettings(c["connectionString"])));
43+
44+
.AddIniFile("Settings.ini")
45+
.AddCommandLine(args)
46+
.AddEnvironmentVariables()
47+
48+
.Build();
49+
50+
config["a_setting_from_sss"];
51+
52+
```
53+
54+
This code assumes that the SQL Stream Store database and schema have been created. If not, it will fail with a SQL Exception
55+
56+
### Writing configuration changes
57+
58+
To write changes, you should create an instance of the **StreamStoreConfigRepository** class. This class allows you to modify the configuration:
59+
60+
Note, the actual writing of configuration settings is idempotent. If you try write the same configuration twice, it will not fail.
61+
62+
``` c#
63+
64+
var repo = new StreamStoreConfigRepository(store);
65+
66+
// Get the latest version, modify it, then write it back again.
67+
var settings = repo.GetLatest(ct);
68+
69+
// The configuration settings object is immutable, but you can create a modified
70+
// version and write that back.
71+
var modified = settings.WithModifiedSettings(("setting1", "newValue"));
72+
await _streamStoreConfigRepository.WriteChanges(modified, ct);
73+
74+
// Modify individual settings.
75+
// Note, in the case of a concurrency error, this will throw a SSS Version Mismatch exception
76+
await repo.Modify(ct,
77+
("setting1", "new value"),
78+
("setting2", "newer value"));
79+
80+
// To handle concurrency errors, you can also use the following method. When a concurrency error occurs,
81+
// you can retry a number of times.
82+
_streamStoreConfigRepository.Modify(
83+
84+
// Delegate that allows you to modify the data
85+
changeSettings: async (currentSettings, ct) =>
86+
{
87+
return currentSettings.WithModifiedSettings(("setting1", value));
88+
},
89+
90+
// Error handling logic, including retries.
91+
errorHandler: (exception, retryCount) =>
92+
{
93+
// do some logging / determine if you want to retry.
94+
return Task.FromResult(true); // returning true, which means retrying.
95+
},
96+
ct: CancellationToken.None);
97+
98+
```
99+
100+
### Monitoring for changes in config
101+
102+
It's not difficult to monitor the configuration for changes.
103+
104+
``` c#
105+
var sssConfig = new ConfigurationBuilder()
106+
107+
// Add the stream store configuration data
108+
.AddStreamStore(
109+
(c) => new MsSqlStreamStore(new MsSqlStreamStoreSettings(c["connectionString"])),
110+
subscribeToChanges: true)
111+
.Build();
112+
113+
// Then use the ChangeToken class to monitor for changes:
114+
ChangeToken.OnChange(sssConfig.GetReloadToken, () =>
115+
{
116+
Console.WriteLine("Settings changed:");
117+
});
118+
119+
```
120+
121+
Note, you can also use your own **IChangeMonitor** implementation.
122+
123+
### Capturing SSS instance or bring your own
124+
125+
The Microsoft.Extensions.Config classes don't implement IDisposable, so it's recommended to either inject your own version of sql stream store
126+
or capture it while it's being built.
127+
128+
``` c#
129+
IStreamStore _captured;
130+
131+
var sssConfig = new ConfigurationBuilder()
132+
.AddStreamStore(
133+
(c) => _captured = new MsSqlStreamStore(new MsSqlStreamStoreSettings(c["connectionString"])))
134+
.Build();
135+
136+
// This allows you to dispose it when you need to. This also cancels any subscriptions:
137+
_captured.Dispose();
138+
139+
```
140+
141+
### Encrypting data at rest
142+
143+
Use the **IConfigurationSettingsHooks** interface to handle your own encryption. Don't roll your own, use a trusted encryption mechanism. Like (https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=netframework-4.7.2)
144+
145+
146+
### Handling database not yet available during startup.
147+
148+
Sometimes the database is not available during startup. Ideally, I'd just like to terminate the process 'gracefully' and have some form of restart logic in the process orchestrator. However, if this is not possible, then you can also implement an error handler:
149+
150+
``` c#
151+
152+
var sssConfig = new ConfigurationBuilder()
153+
154+
// Add the stream store configuration data
155+
.AddStreamStore(
156+
// When an error occurs while connecting to the database
157+
(c) => _captured = new MsSqlStreamStore(new MsSqlStreamStoreSettings(c["connectionString"])),
158+
errorHandler: OnConnectError
159+
160+
private static async Task<bool> OnConnectError(Exception ex, int retryCount)
161+
{
162+
// delay before retrying???
163+
await Task.Delay(2000);
164+
165+
// Logging?
166+
167+
// Retry? returning true like this retries indefinitely.
168+
return true;
169+
}
170+
171+
```
172+
173+
### retrieving historic settings and reverting.
174+
175+
``` c#
176+
177+
// Gets a list of all stored versions.
178+
var history = await _streamStoreConfigRepository.GetSettingsHistory(CancellationToken.None);
179+
180+
// Gets a specific version.
181+
var version1 = await _streamStoreConfigRepository.GetSpecificVersion(1, CancellationToken.None);
182+
183+
// Reverts config back to a previous version
184+
await _streamStoreConfigRepository.RevertToVersion(version1, CancellationToken.None);
185+
186+
```
187+
7188
## Licencing
8189

9190
Licenced under [MIT](https://opensource.org/licenses/MIT).

src/Config.SqlStreamStore.Tests/ConfigRepositoryTests.cs

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
using System;
22
using System.Linq;
3+
using System.Text;
34
using System.Threading;
45
using System.Threading.Tasks;
6+
using Newtonsoft.Json;
57
using SqlStreamStore;
68
using Xunit;
79

810
namespace Config.SqlStreamStore.Tests
911
{
1012

11-
// Max number of versions
12-
// Can roll back to a version
13-
// List versions
14-
// Hooks for encryption / decryption
15-
//
16-
1713
public class ConfigRepositoryTests
1814
{
1915
private StreamStoreConfigRepository _streamStoreConfigRepository;
2016

2117
public ConfigRepositoryTests()
2218
{
23-
_streamStoreConfigRepository = new StreamStoreConfigRepository(new InMemoryStreamStore());
19+
_streamStoreConfigRepository = new StreamStoreConfigRepository(new InMemoryStreamStore(),
20+
messageHooks: new Base64Hook());
2421
}
2522

2623
[Fact]
@@ -32,7 +29,7 @@ public async Task Can_save_new_settings()
3229

3330
var result = await _streamStoreConfigRepository.WriteChanges(settings, CancellationToken.None);
3431
Assert.Equal(0, result.Version);
35-
32+
Assert.Equal(new []{"setting1", "setting2"}, result.ModifiedKeys);
3633
var saved = await _streamStoreConfigRepository.GetLatest(CancellationToken.None);
3734

3835
Assert.Equal(settings, saved);
@@ -63,7 +60,7 @@ public async Task Can_delete_existing_settings()
6360
Assert.NotEqual(settings, modified);
6461

6562
var saved = await SaveSettings(modified);
66-
63+
Assert.Equal(new[] { "setting1" }, saved.DeletedKeys);
6764
Assert.Equal(modified, saved);
6865
Assert.False(saved.ContainsKey("setting1"));
6966
}
@@ -88,7 +85,7 @@ Task OnSettingsChanged(IConfigurationSettings configurationSettings, Cancellatio
8885
return Task.CompletedTask;
8986
}
9087

91-
var subscription = _streamStoreConfigRepository.SubscribeToChanges(settings.Version, OnSettingsChanged,
88+
var subscription = _streamStoreConfigRepository.WatchForChanges(settings.Version, OnSettingsChanged,
9289
ct: CancellationToken.None);
9390

9491
var modified = await _streamStoreConfigRepository.WriteChanges(settings.WithModifiedSettings(("setting1", "newValue")), CancellationToken.None);
@@ -191,12 +188,56 @@ await _streamStoreConfigRepository.Modify(CancellationToken.None,
191188
Assert.Equal(expectedMaxCount, history.Count);
192189
}
193190

191+
[Fact]
192+
public async Task Can_revert_to_previous_version()
193+
{
194+
// Write 5 modifications.
195+
for (int i = 0; i < 5; i++)
196+
{
197+
await _streamStoreConfigRepository.Modify(CancellationToken.None,
198+
("setting", i.ToString()),
199+
("othersetting", "constant")
200+
);
201+
}
202+
203+
var version1 = await _streamStoreConfigRepository.GetSpecificVersion(1, CancellationToken.None);
204+
205+
await _streamStoreConfigRepository.RevertToVersion(version1, CancellationToken.None);
206+
207+
var latest = await _streamStoreConfigRepository.GetLatest(CancellationToken.None);
208+
Assert.Equal(version1["setting"], latest["setting"]);
209+
}
210+
194211
private static ModifiedConfigurationSettings BuildNewSettings()
195212
{
196213
var settings = new ModifiedConfigurationSettings(
197214
("setting1", "value1"),
198215
("setting2", "setting2"));
199216
return settings;
200217
}
218+
219+
/// <summary>
220+
/// This hook converts the input / output to and from Base64, just to see if the
221+
/// hook actually works.
222+
///
223+
/// Note, this is NOT encryption, nor should it ever be confused with encryption.
224+
/// If you consider using this as a form of encryption, you should reconsider your
225+
/// life choices that lead you up to this.
226+
/// </summary>
227+
private class Base64Hook : IConfigurationSettingsHooks
228+
{
229+
public string OnReadMessage(string message)
230+
{
231+
return Encoding.UTF8.GetString(Convert.FromBase64String(message));
232+
}
233+
234+
public string OnWriteMessage(string message)
235+
{
236+
return Convert.ToBase64String(Encoding.UTF8.GetBytes(message));
237+
238+
}
239+
240+
public JsonSerializerSettings JsonSerializerSettings => new JsonSerializerSettings();
241+
}
201242
}
202243
}

src/Config.SqlStreamStore/ConfigChanged.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Config.SqlStreamStore
44
{
5+
/// <summary>
6+
/// Message that's saved in SSS to record configuration has changed.
7+
/// </summary>
58
public class ConfigChanged
69
{
710
public ConfigChanged()
@@ -16,8 +19,19 @@ public ConfigChanged(Dictionary<string, string> allSettings, HashSet<string> mod
1619
DeletedSettings = deletedSettings;
1720
}
1821

22+
/// <summary>
23+
/// Memento of all settings.
24+
/// </summary>
1925
public Dictionary<string, string> AllSettings { get; set; }
26+
27+
/// <summary>
28+
/// Pointers to the settings that have changed in this version
29+
/// </summary>
2030
public HashSet<string> ModifiedSettings { get; set; }
31+
32+
/// <summary>
33+
/// Names of the keys that have been deleted in this version.
34+
/// </summary>
2135
public HashSet<string> DeletedSettings { get; set; }
2236
}
2337
}

0 commit comments

Comments
 (0)