├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── develop.yml
│ └── release.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── images
├── configuration-service-fs.png
├── configuration-service-git.png
├── configuration-service-vault.png
└── configuration-service.gif
└── src
├── ConfigurationService.Client
├── ConfigurationOptions.cs
├── ConfigurationService.Client.csproj
├── IConfigurationParser.cs
├── ISubscriber.cs
├── Logger.cs
├── Parsers
│ ├── IniConfigurationFileParser.cs
│ ├── JsonConfigurationFileParser.cs
│ ├── XmlConfigurationFileParser.cs
│ └── YamlConfigurationFileParser.cs
├── RemoteConfigurationBuilder.cs
├── RemoteConfigurationException.cs
├── RemoteConfigurationExtensions.cs
├── RemoteConfigurationOptions.cs
├── RemoteConfigurationProvider.cs
├── RemoteConfigurationSource.cs
└── Subscribers
│ ├── Nats
│ └── NatsSubscriber.cs
│ ├── RabbitMq
│ ├── RabbitMqOptions.cs
│ └── RabbitMqSubscriber.cs
│ └── Redis
│ └── RedisSubscriber.cs
├── ConfigurationService.Hosting
├── ConfigurationEndpointRouteBuilderExtensions.cs
├── ConfigurationService.Hosting.csproj
├── ConfigurationService.cs
├── ConfigurationServiceBuilder.cs
├── ConfigurationServiceBuilderExtensions.cs
├── Extensions
│ └── StringExtensions.cs
├── Hasher.cs
├── HostedConfigurationService.cs
├── IConfigurationService.cs
├── IConfigurationServiceBuilder.cs
├── ProviderOptionNullException.cs
├── Providers
│ ├── FileSystem
│ │ ├── FileSystemProvider.cs
│ │ └── FileSystemProviderOptions.cs
│ ├── Git
│ │ ├── GitProvider.cs
│ │ └── GitProviderOptions.cs
│ ├── IProvider.cs
│ └── Vault
│ │ ├── VaultProvider.cs
│ │ └── VaultProviderOptions.cs
└── Publishers
│ ├── IPublisher.cs
│ ├── Nats
│ └── NatsPublisher.cs
│ ├── RabbitMq
│ ├── RabbitMqOptions.cs
│ └── RabbitMqPublisher.cs
│ └── Redis
│ └── RedisPublisher.cs
├── ConfigurationService.Test
├── ConfigurationService.Test.csproj
├── Files
│ ├── test.ini
│ ├── test.json
│ ├── test.xml
│ └── test.yaml
├── ParserTests.cs
└── PublishTests.cs
├── ConfigurationService.sln
├── ConfigurationService.sln.DotSettings
└── samples
├── ConfigurationService.Samples.Client
├── ConfigWriter.cs
├── ConfigurationService.Samples.Client.csproj
├── Program.cs
├── TestConfig.cs
└── appsettings.json
└── ConfigurationService.Samples.Host
├── ConfigurationService.Samples.Host.csproj
├── Program.cs
├── Properties
└── launchSettings.json
├── appsettings.Development.json
└── appsettings.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/develop.yml:
--------------------------------------------------------------------------------
1 | name: develop
2 |
3 | on:
4 | push:
5 | branches-ignore: [ main ]
6 | pull_request:
7 | branches-ignore: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 6.0.x
20 | - name: Install dependencies
21 | run: dotnet restore src/ConfigurationService.sln
22 | - name: Build
23 | run: dotnet build src/ConfigurationService.sln --configuration Release --no-restore
24 | - name: Test
25 | run: dotnet test src/ConfigurationService.sln --no-restore --logger "console;verbosity=normal"
26 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 6.0.x
20 |
21 | - name: Install dependencies
22 | run: dotnet restore src/ConfigurationService.sln
23 |
24 | - name: Build
25 | run: dotnet build src/ConfigurationService.sln --configuration Release --no-restore
26 |
27 | - name: Test
28 | run: dotnet test src/ConfigurationService.sln --no-restore --logger "console;verbosity=normal"
29 |
30 | - name: Publish ConfigurationService.Hosting
31 | uses: brandedoutcast/publish-nuget@v2.5.2
32 | with:
33 | PROJECT_FILE_PATH: src/ConfigurationService.Hosting/ConfigurationService.Hosting.csproj
34 | NUGET_KEY: ${{secrets.NUGET_API_KEY}}
35 | TAG_COMMIT: false
36 |
37 | - name: Publish ConfigurationService.Client
38 | uses: brandedoutcast/publish-nuget@v2.5.2
39 | with:
40 | PROJECT_FILE_PATH: src/ConfigurationService.Client/ConfigurationService.Client.csproj
41 | NUGET_KEY: ${{secrets.NUGET_API_KEY}}
42 | TAG_COMMIT: false
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Configuration Service
2 | We want to make contributing to this project as easy and transparent as possible, whether it's:
3 |
4 | - Reporting a bug
5 | - Discussing the current state of the code
6 | - Submitting a fix
7 | - Proposing new features
8 | - Becoming a maintainer
9 |
10 | ## We Develop with Github
11 | We use github to host code, to track issues and feature requests, as well as accept pull requests.
12 |
13 | 1. Fork the repo and create your branch from `main`.
14 | 2. If you've added code that should be tested, add tests.
15 | 3. If you've changed APIs, update the documentation.
16 | 4. Ensure the test suite passes.
17 | 5. Make sure your code lints.
18 | 6. Issue that pull request!
19 |
20 | ## Any contributions you make will be under the MIT Software License
21 | When you submit code changes, your submissions are understood to be under the same [MIT License](https://github.com/jamespratt/configuration-service/blob/main/LICENSE) that covers the project.
22 | Feel free to contact the maintainers if that's a concern.
23 |
24 | ## Report bugs using Github's [issues](https://github.com/jamespratt/configuration-service/issues)
25 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/jamespratt/configuration-service/issues); it's that easy!
26 |
27 | **Great Bug Reports** tend to have:
28 |
29 | - A quick summary and/or background
30 | - Steps to reproduce
31 | - Be specific!
32 | - Give sample code if you can.
33 | - What you expected would happen
34 | - What actually happens
35 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
36 |
37 | ## License
38 | By contributing, you agree that your contributions will be licensed under its MIT License.
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Configuration Service
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Configuration Service
2 |
3 | [](https://github.com/jamespratt/configuration-service/actions?query=workflow%3Arelease)
4 |
5 |
6 | | Package |Latest Release|
7 | |:----------|:------------:|
8 | |**ConfigurationService.Hosting**|[](https://www.nuget.org/packages/ConfigurationService.Hosting)
9 | |**ConfigurationService.Client**|[](https://www.nuget.org/packages/ConfigurationService.Client)
10 |
11 |
12 |
13 | ## About Configuration Service
14 |
15 | Configuration Service is a distributed configuration service for .NET. Configuration for fleets of applications, services, and containerized micro-services can be updated immediately without the need to redeploy or restart. Configuration Service uses a client/server pub/sub architecture to notify subscribed clients of configuration changes as they happen. Configuration can be injected using the standard options pattern with `IOptions`, `IOptionsMonitor` or `IOptionsSnapshot`.
16 |
17 | Configuration Service currently supports hosting configuration with git, file system or Vault backends and supports publishing changes with Redis, NATS or RabbitMQ publish/subscribe. File types supported are .json, .yaml, .xml and .ini.
18 |
19 | [](#about-configuration-service)
20 |
21 | ## Features
22 | * RESTful HTTP based API for external configuration.
23 | * Server easily integrates into an ASP.NET application.
24 | * Client easily integrates into any .NET Standard 2.0 application using the standard `ConfigurationBuilder` pattern.
25 | * Client encapsulates real-time configuration updates.
26 | * Support for git, file system and Vault backend storage.
27 | * Support for pub/sub with Redis, NATS and RabbitMQ.
28 | * Support for .json, .yaml, .xml and .ini configuration files.
29 | * Inject configuration with `IOptionsMonitor` or `IOptionsSnapshot` to access configuration changes.
30 |
31 | ## Installing with NuGet
32 |
33 | The easiest way to install Configuration Service is with [NuGet](https://www.nuget.org/packages/ConfigurationService.Hosting/).
34 |
35 | In Visual Studio's [Package Manager Console](http://docs.nuget.org/docs/start-here/using-the-package-manager-console),
36 | enter the following command:
37 |
38 | Server:
39 |
40 | Install-Package ConfigurationService.Hosting
41 |
42 | Client:
43 |
44 | Install-Package ConfigurationService.Client
45 |
46 | ## Adding the Configuration Service Host
47 | The Configuration Service host middleware can be added to the service collection of an existing ASP.NET application. The following example configures a git storage provider with a Redis publisher.
48 |
49 | ```csharp
50 | builder.Services.AddConfigurationService()
51 | .AddGitProvider(c =>
52 | {
53 | c.RepositoryUrl = "https://github.com/jamespratt/configuration-test.git";
54 | c.LocalPath = "C:/local-repo";
55 | })
56 | .AddRedisPublisher("localhost:6379");
57 | ```
58 |
59 | In Startup.Configure, call `MapConfigurationService` on the endpoint builder. The default pattern is "/configuration".
60 |
61 | ```csharp
62 | app.UseEndpoints(endpoints =>
63 | {
64 | endpoints.MapConfigurationService();
65 | });
66 | ```
67 |
68 | The configured host will expose two API endpoints:
69 | * `configuration/` - Lists all files at the configured provider.
70 | * `configuration/{filename}` - Retrieves the contents of the specified file.
71 |
72 | #### Git Provider Options
73 | | Property | Description |
74 | |:-----------|:------------|
75 | |RepositoryUrl|URI for the remote repository.|
76 | |Username|Username for authentication.|
77 | |Password|Password for authentication.|
78 | |Branch|The name of the branch to checkout. When unspecified the remote's default branch will be used instead.|
79 | |LocalPath|Local path to clone into.|
80 | |SearchPattern|The search string to use as a filter against the names of files. Defaults to no filter (\*).|
81 | |PollingInterval|The interval to check for remote changes. Defaults to 60 seconds.|
82 |
83 | ```csharp
84 | services.AddConfigurationService()
85 | .AddGitProvider(c =>
86 | {
87 | c.RepositoryUrl = "https://example.com/my-repo/my-repo.git";
88 | c.Username = "username";
89 | c.Password = "password";
90 | c.Branch = "main";
91 | c.LocalPath = "C:/config";
92 | c.SearchPattern = ".*json";
93 | c.PollingInterval = TimeSpan.FromSeconds(60);
94 | }
95 | ...
96 | ```
97 |
98 | #### File System Provider Options
99 | | Property | Description |
100 | |:-----------|:------------|
101 | |Path|Path to the configuration files.|
102 | |SearchPattern|The search string to use as a filter against the names of files. Defaults to no filter (\*).|
103 | |IncludeSubdirectories|Includes the current directory and all its subdirectories. Defaults to `false`.|
104 | |Username|Username for authentication.|
105 | |Password|Password for authentication.|
106 | |Domain|Domain for authentication.|
107 |
108 | ```csharp
109 | services.AddConfigurationService()
110 | .AddFileSystemProvider(c =>
111 | {
112 | c.Path = "C:/config";
113 | c.SearchPattern = "*.json";
114 | c.IncludeSubdirectories = true;
115 | })
116 | ...
117 | ```
118 |
119 | #### Vault Provider Options
120 | | Property | Description |
121 | |:-----------|:------------|
122 | |ServerUri|The Vault Server Uri with port.|
123 | |Path|The path where the kv secrets engine is enabled.|
124 | |AuthMethodInfo|The auth method to be used to acquire a Vault token.|
125 | |PollingInterval|The interval to check for for remote changes. Defaults to 60 seconds.|
126 |
127 | ```csharp
128 | services.AddConfigurationService()
129 | .AddVaultProvider(c =>
130 | {
131 | c.ServerUri = "http://localhost:8200/";
132 | c.Path = "secret/";
133 | c.AuthMethodInfo = new TokenAuthMethodInfo("myToken");
134 | })
135 | ...
136 | ```
137 |
138 | #### Custom Storage Providers and Publishers
139 | Custom implementations of storage providers and publishers can be added by implementing the `IProvider` and `IPublisher` interfaces and calling the appropriate extension methods on AddConfigurationService:
140 |
141 | ```csharp
142 | services.AddConfigurationService()
143 | .AddProvider(new CustomStorageProvider())
144 | .AddPublisher(new CustomPublisher());
145 | ```
146 |
147 | ## Adding the Configuration Service Client
148 | The Configuration Service client can be configured by adding `AddRemoteConfiguration` to the standard configuration builder. In the following example, remote json configuration is added and a Redis endpoint is specified for configuration change subscription. Local configuration can be read for settings for the remote source by using multiple instances of configuration builder.
149 |
150 | ```csharp
151 | var loggerFactory = LoggerFactory.Create(builder =>
152 | {
153 | builder.AddConsole();
154 | });
155 |
156 | IConfiguration configuration = new ConfigurationBuilder()
157 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
158 | .Build();
159 |
160 | configuration = new ConfigurationBuilder()
161 | .AddConfiguration(configuration)
162 | .AddRemoteConfiguration(o =>
163 | {
164 | o.ServiceUri = "http://localhost:5000/configuration/";
165 | o.AddConfiguration(c =>
166 | {
167 | c.ConfigurationName = "test.json";
168 | c.ReloadOnChange = true;
169 | c.Optional = false;
170 | });
171 | o.AddConfiguration(c =>
172 | {
173 | c.ConfigurationName = "test.yaml";
174 | c.ReloadOnChange = true;
175 | c.Optional = false;
176 | c.Parser = new YamlConfigurationFileParser();
177 | });
178 | o.AddRedisSubscriber("localhost:6379");
179 | o.AddLoggerFactory(loggerFactory);
180 | })
181 | .Build();
182 | ```
183 |
184 | #### Configuration Soruce Options
185 | | Property | Description |
186 | |:-----------|:------------|
187 | |ServiceUri|Configuration service endpoint.|
188 | |HttpMessageHandler|The optional `HttpMessageHandler` for the `HttpClient`.|
189 | |RequestTimeout|The timeout for the `HttpClient` request to the configuration server. Defaults to 60 seconds.|
190 | |LoggerFactory|The type used to configure the logging system and create instances of `ILogger`. Defaults to `NullLoggerFactory`.|
191 | |**AddConfiguration**|Adds an individual configuration file.|
192 | |ConfigurationName|Path or name of the configuration file relative to the configuration provider. This value should match the value specified in the list returned by the `configuration/` endpoint.|
193 | |Optional|Determines if loading the file is optional.|
194 | |ReloadOnChange|Determines whether the source will be loaded if the underlying file changes.|
195 | |Parser|The type used to parse the remote configuration file. The client will attempt to resolve this from the file extension of `ConfigurationName` if not specified.
Supported Types:
- `JsonConfigurationFileParser`
- `YamlConfigurationFileParser`
- `XmlConfigurationFileParser`
- `IniConfigurationFileParser`
|
196 | |**AddRedisSubscriber**|Adds Redis as the configuration subscriber.|
197 | |**AddNatsSubscriber**|Adds NATS as the configuration subscriber.|
198 | |**AddRabbitMqSubscriber**|Adds RabbitMQ as the configuration subscriber.|
199 | |**AddSubscriber**|Adds a custom configuration subscriber the implements `ISubscriber`.|
200 | |**AddLoggerFactory**|Adds the type used to configure the logging system and create instances of `ILogger`.|
201 |
202 | ## Samples
203 | Samples of both host and client implementations can be viewed at [Samples](https://github.com/jamespratt/configuration-service/tree/main/src/samples).
204 |
205 | [](https://github.com/jamespratt/configuration-service/actions?query=workflow%3Arelease)
206 |
--------------------------------------------------------------------------------
/images/configuration-service-fs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamespratt/configuration-service/1011f3bbf2af857a000d2e379ba960625024ffa5/images/configuration-service-fs.png
--------------------------------------------------------------------------------
/images/configuration-service-git.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamespratt/configuration-service/1011f3bbf2af857a000d2e379ba960625024ffa5/images/configuration-service-git.png
--------------------------------------------------------------------------------
/images/configuration-service-vault.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamespratt/configuration-service/1011f3bbf2af857a000d2e379ba960625024ffa5/images/configuration-service-vault.png
--------------------------------------------------------------------------------
/images/configuration-service.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamespratt/configuration-service/1011f3bbf2af857a000d2e379ba960625024ffa5/images/configuration-service.gif
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/ConfigurationOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ConfigurationService.Client
2 | {
3 | public class ConfigurationOptions
4 | {
5 | ///
6 | /// Name or path of the configuration file relative to the configuration provider path.
7 | ///
8 | public string ConfigurationName { get; set; }
9 |
10 | ///
11 | /// Determines if loading the file is optional. Defaults to false>.
12 | ///
13 | public bool Optional { get; set; }
14 |
15 | ///
16 | /// Determines whether the source will be loaded if the underlying file changes. Defaults to false.
17 | ///
18 | public bool ReloadOnChange { get; set; }
19 |
20 | ///
21 | /// The type of used to parse the remote configuration file.
22 | ///
23 | public IConfigurationParser Parser { get; set; }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/ConfigurationService.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Contains the client configuration provider, file parsers and subscriber components for Configuration Service.
6 | Configuration Service is a distributed configuration service for .NET.
7 |
8 | netstandard2.0
9 | 1.0.2
10 | James Pratt
11 | ConfigurationService.Client
12 | ConfigurationService.Client
13 |
14 |
15 |
16 | true
17 | $(NoWarn);1591
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/IConfigurationParser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 |
4 | namespace ConfigurationService.Client
5 | {
6 | public interface IConfigurationParser
7 | {
8 | IDictionary Parse(Stream input);
9 | }
10 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/ISubscriber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ConfigurationService.Client
4 | {
5 | public interface ISubscriber
6 | {
7 | string Name { get; }
8 |
9 | void Initialize();
10 |
11 | void Subscribe(string topic, Action handler);
12 | }
13 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Logger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using Microsoft.Extensions.Logging.Abstractions;
3 |
4 | namespace ConfigurationService.Client
5 | {
6 | public static class Logger
7 | {
8 | private static ILoggerFactory _factory;
9 |
10 | public static ILoggerFactory LoggerFactory
11 | {
12 | get => _factory = _factory ?? new NullLoggerFactory();
13 | set => _factory = value;
14 | }
15 |
16 | public static ILogger CreateLogger() => LoggerFactory.CreateLogger();
17 | }
18 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Parsers/IniConfigurationFileParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using Microsoft.Extensions.Configuration;
5 |
6 | namespace ConfigurationService.Client.Parsers
7 | {
8 | public class IniConfigurationFileParser : IConfigurationParser
9 | {
10 | private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase);
11 |
12 | public IDictionary Parse(Stream input) => ParseStream(input);
13 |
14 | private IDictionary ParseStream(Stream input)
15 | {
16 | _data.Clear();
17 |
18 | using (var reader = new StreamReader(input))
19 | {
20 | var sectionPrefix = string.Empty;
21 |
22 | while (reader.Peek() != -1)
23 | {
24 | var rawLine = reader.ReadLine();
25 | if (rawLine == null)
26 | {
27 | continue;
28 | }
29 |
30 | var line = rawLine.Trim();
31 |
32 | if (string.IsNullOrWhiteSpace(line))
33 | {
34 | continue;
35 | }
36 |
37 | if (line[0] == ';' || line[0] == '#' || line[0] == '/')
38 | {
39 | continue;
40 | }
41 |
42 | if (line[0] == '[' && line[line.Length - 1] == ']')
43 | {
44 | sectionPrefix = line.Substring(1, line.Length - 2) + ConfigurationPath.KeyDelimiter;
45 | continue;
46 | }
47 |
48 | int separator = line.IndexOf('=');
49 | if (separator < 0)
50 | {
51 | throw new FormatException($"Unrecognized line format '{rawLine}'.");
52 | }
53 |
54 | string key = sectionPrefix + line.Substring(0, separator).Trim();
55 | string value = line.Substring(separator + 1).Trim();
56 |
57 | if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')
58 | {
59 | value = value.Substring(1, value.Length - 2);
60 | }
61 |
62 | if (_data.ContainsKey(key))
63 | {
64 | throw new FormatException($"A duplicate key '{key}' was found.");
65 | }
66 |
67 | _data[key] = value;
68 | }
69 | }
70 | return _data;
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Parsers/JsonConfigurationFileParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text.Json;
6 | using Microsoft.Extensions.Configuration;
7 |
8 | namespace ConfigurationService.Client.Parsers
9 | {
10 | public class JsonConfigurationFileParser : IConfigurationParser
11 | {
12 | private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase);
13 | private readonly Stack _context = new Stack();
14 | private string _currentPath;
15 |
16 | public IDictionary Parse(Stream input) => ParseStream(input);
17 |
18 | private IDictionary ParseStream(Stream input)
19 | {
20 | _data.Clear();
21 |
22 | var jsonDocumentOptions = new JsonDocumentOptions
23 | {
24 | CommentHandling = JsonCommentHandling.Skip,
25 | AllowTrailingCommas = true,
26 | };
27 |
28 | using (var reader = new StreamReader(input))
29 | using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
30 | {
31 | if (doc.RootElement.ValueKind != JsonValueKind.Object)
32 | {
33 | throw new FormatException($"Unsupported JSON token '{doc.RootElement.ValueKind}' was found.");
34 | }
35 | VisitElement(doc.RootElement);
36 | }
37 |
38 | return _data;
39 | }
40 |
41 | private void VisitElement(JsonElement element)
42 | {
43 | foreach (var property in element.EnumerateObject())
44 | {
45 | EnterContext(property.Name);
46 | VisitValue(property.Value);
47 | ExitContext();
48 | }
49 | }
50 |
51 | private void VisitValue(JsonElement value)
52 | {
53 | switch (value.ValueKind)
54 | {
55 | case JsonValueKind.Object:
56 | VisitElement(value);
57 | break;
58 |
59 | case JsonValueKind.Array:
60 | var index = 0;
61 | foreach (var arrayElement in value.EnumerateArray())
62 | {
63 | EnterContext(index.ToString());
64 | VisitValue(arrayElement);
65 | ExitContext();
66 | index++;
67 | }
68 | break;
69 |
70 | case JsonValueKind.Number:
71 | case JsonValueKind.String:
72 | case JsonValueKind.True:
73 | case JsonValueKind.False:
74 | case JsonValueKind.Null:
75 | var key = _currentPath;
76 | if (_data.ContainsKey(key))
77 | {
78 | throw new FormatException($"A duplicate key '{key}' was found.");
79 | }
80 | _data[key] = value.ToString();
81 | break;
82 |
83 | default:
84 | throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
85 | }
86 | }
87 |
88 | private void EnterContext(string context)
89 | {
90 | _context.Push(context);
91 | _currentPath = ConfigurationPath.Combine(_context.Reverse());
92 | }
93 |
94 | private void ExitContext()
95 | {
96 | _context.Pop();
97 | _currentPath = ConfigurationPath.Combine(_context.Reverse());
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Parsers/XmlConfigurationFileParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Xml;
6 | using Microsoft.Extensions.Configuration;
7 |
8 | namespace ConfigurationService.Client.Parsers
9 | {
10 | public class XmlConfigurationFileParser : IConfigurationParser
11 | {
12 | private const string NameAttributeKey = "Name";
13 |
14 | private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase);
15 |
16 | public IDictionary Parse(Stream input) => ParseStream(input);
17 |
18 | private IDictionary ParseStream(Stream input)
19 | {
20 | _data.Clear();
21 |
22 | var readerSettings = new XmlReaderSettings()
23 | {
24 | CloseInput = false,
25 | DtdProcessing = DtdProcessing.Prohibit,
26 | IgnoreComments = true,
27 | IgnoreWhitespace = true
28 | };
29 |
30 | using (var reader = CreateXmlReader(input, readerSettings))
31 | {
32 | var prefixStack = new Stack();
33 |
34 | SkipUntilRootElement(reader);
35 |
36 | ProcessAttributes(reader, prefixStack, _data, AddNamePrefix);
37 | ProcessAttributes(reader, prefixStack, _data, AddAttributePair);
38 |
39 | var preNodeType = reader.NodeType;
40 | while (reader.Read())
41 | {
42 | switch (reader.NodeType)
43 | {
44 | case XmlNodeType.Element:
45 | prefixStack.Push(reader.LocalName);
46 | ProcessAttributes(reader, prefixStack, _data, AddNamePrefix);
47 | ProcessAttributes(reader, prefixStack, _data, AddAttributePair);
48 |
49 | if (reader.IsEmptyElement)
50 | {
51 | prefixStack.Pop();
52 | }
53 | break;
54 |
55 | case XmlNodeType.EndElement:
56 | if (prefixStack.Any())
57 | {
58 | if (preNodeType == XmlNodeType.Element)
59 | {
60 | var key = ConfigurationPath.Combine(prefixStack.Reverse());
61 | _data[key] = string.Empty;
62 | }
63 |
64 | prefixStack.Pop();
65 | }
66 | break;
67 |
68 | case XmlNodeType.CDATA:
69 | case XmlNodeType.Text:
70 | {
71 | var key = ConfigurationPath.Combine(prefixStack.Reverse());
72 | if (_data.ContainsKey(key))
73 | {
74 | throw new FormatException($"A duplicate key '{key}' was found.");
75 | }
76 |
77 | _data[key] = reader.Value;
78 | break;
79 | }
80 | case XmlNodeType.XmlDeclaration:
81 | case XmlNodeType.ProcessingInstruction:
82 | case XmlNodeType.Comment:
83 | case XmlNodeType.Whitespace:
84 | break;
85 |
86 | default:
87 | throw new FormatException($"Unsupported node type '{reader.NodeType}' was found.");
88 | }
89 | preNodeType = reader.NodeType;
90 |
91 | if (preNodeType == XmlNodeType.Element &&
92 | reader.IsEmptyElement)
93 | {
94 | preNodeType = XmlNodeType.EndElement;
95 | }
96 | }
97 | }
98 |
99 | return _data;
100 | }
101 |
102 | private XmlReader CreateXmlReader(Stream input, XmlReaderSettings settings)
103 | {
104 | var memStream = new MemoryStream();
105 | input.CopyTo(memStream);
106 | memStream.Position = 0;
107 |
108 | var document = new XmlDocument();
109 | using (var reader = XmlReader.Create(memStream, settings))
110 | {
111 | document.Load(reader);
112 | }
113 | memStream.Position = 0;
114 |
115 | return XmlReader.Create(memStream, settings);
116 | }
117 |
118 | private void SkipUntilRootElement(XmlReader reader)
119 | {
120 | while (reader.Read())
121 | {
122 | if (reader.NodeType != XmlNodeType.XmlDeclaration &&
123 | reader.NodeType != XmlNodeType.ProcessingInstruction)
124 | {
125 | break;
126 | }
127 | }
128 | }
129 |
130 | private void ProcessAttributes(XmlReader reader, Stack prefixStack, IDictionary data,
131 | Action, IDictionary, XmlWriter> act, XmlWriter writer = null)
132 | {
133 | for (int i = 0; i < reader.AttributeCount; i++)
134 | {
135 | reader.MoveToAttribute(i);
136 |
137 | if (!string.IsNullOrEmpty(reader.NamespaceURI))
138 | {
139 | throw new FormatException("Namespace is not supported.");
140 | }
141 |
142 | act(reader, prefixStack, data, writer);
143 | }
144 |
145 | reader.MoveToElement();
146 | }
147 |
148 | private static void AddNamePrefix(XmlReader reader, Stack prefixStack,
149 | IDictionary data, XmlWriter writer)
150 | {
151 | if (!string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase))
152 | {
153 | return;
154 | }
155 |
156 | if (prefixStack.Any())
157 | {
158 | var lastPrefix = prefixStack.Pop();
159 | prefixStack.Push(ConfigurationPath.Combine(lastPrefix, reader.Value));
160 | }
161 | else
162 | {
163 | prefixStack.Push(reader.Value);
164 | }
165 | }
166 |
167 | private static void AddAttributePair(XmlReader reader, Stack prefixStack,
168 | IDictionary data, XmlWriter writer)
169 | {
170 | prefixStack.Push(reader.LocalName);
171 | var key = ConfigurationPath.Combine(prefixStack.Reverse());
172 | if (data.ContainsKey(key))
173 | {
174 | throw new FormatException($"A duplicate key '{key}' was found.");
175 | }
176 |
177 | data[key] = reader.Value;
178 | prefixStack.Pop();
179 | }
180 | }
181 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Parsers/YamlConfigurationFileParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Microsoft.Extensions.Configuration;
6 | using YamlDotNet.RepresentationModel;
7 |
8 | namespace ConfigurationService.Client.Parsers
9 | {
10 | public class YamlConfigurationFileParser : IConfigurationParser
11 | {
12 | private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase);
13 | private readonly Stack _context = new Stack();
14 | private string _currentPath;
15 |
16 | public IDictionary Parse(Stream input)
17 | {
18 | _data.Clear();
19 | _context.Clear();
20 |
21 | var yaml = new YamlStream();
22 | yaml.Load(new StreamReader(input, detectEncodingFromByteOrderMarks: true));
23 |
24 | if (yaml.Documents.Any())
25 | {
26 | var mapping = (YamlMappingNode)yaml.Documents[0].RootNode;
27 |
28 | VisitYamlMappingNode(mapping);
29 | }
30 |
31 | return _data;
32 | }
33 |
34 | private void VisitYamlNodePair(KeyValuePair yamlNodePair)
35 | {
36 | var context = ((YamlScalarNode)yamlNodePair.Key).Value;
37 | VisitYamlNode(context, yamlNodePair.Value);
38 | }
39 |
40 | private void VisitYamlNode(string context, YamlNode node)
41 | {
42 | if (node is YamlScalarNode scalarNode)
43 | {
44 | VisitYamlScalarNode(context, scalarNode);
45 | }
46 | if (node is YamlMappingNode mappingNode)
47 | {
48 | VisitYamlMappingNode(context, mappingNode);
49 | }
50 | if (node is YamlSequenceNode sequenceNode)
51 | {
52 | VisitYamlSequenceNode(context, sequenceNode);
53 | }
54 | }
55 |
56 | private void VisitYamlScalarNode(string context, YamlScalarNode yamlValue)
57 | {
58 | EnterContext(context);
59 | var key = _currentPath;
60 |
61 | if (_data.ContainsKey(key))
62 | {
63 | throw new FormatException($"A duplicate key '{key}' was found.");
64 | }
65 |
66 | _data[key] = IsNullValue(yamlValue) ? null : yamlValue.Value;
67 | ExitContext();
68 | }
69 |
70 | private void VisitYamlMappingNode(YamlMappingNode node)
71 | {
72 | foreach (var yamlNodePair in node.Children)
73 | {
74 | VisitYamlNodePair(yamlNodePair);
75 | }
76 | }
77 |
78 | private void VisitYamlMappingNode(string context, YamlMappingNode yamlValue)
79 | {
80 | EnterContext(context);
81 |
82 | VisitYamlMappingNode(yamlValue);
83 |
84 | ExitContext();
85 | }
86 |
87 | private void VisitYamlSequenceNode(string context, YamlSequenceNode yamlValue)
88 | {
89 | EnterContext(context);
90 |
91 | VisitYamlSequenceNode(yamlValue);
92 |
93 | ExitContext();
94 | }
95 |
96 | private void VisitYamlSequenceNode(YamlSequenceNode node)
97 | {
98 | for (int i = 0; i < node.Children.Count; i++)
99 | {
100 | VisitYamlNode(i.ToString(), node.Children[i]);
101 | }
102 | }
103 |
104 | private void EnterContext(string context)
105 | {
106 | _context.Push(context);
107 | _currentPath = ConfigurationPath.Combine(_context.Reverse());
108 | }
109 |
110 | private void ExitContext()
111 | {
112 | _context.Pop();
113 | _currentPath = ConfigurationPath.Combine(_context.Reverse());
114 | }
115 |
116 | private bool IsNullValue(YamlScalarNode yamlValue)
117 | {
118 | return yamlValue.Style == YamlDotNet.Core.ScalarStyle.Plain
119 | && (
120 | yamlValue.Value == "~"
121 | || yamlValue.Value == "null"
122 | || yamlValue.Value == "Null"
123 | || yamlValue.Value == "NULL"
124 | );
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Microsoft.Extensions.Configuration;
4 |
5 | namespace ConfigurationService.Client
6 | {
7 | internal class RemoteConfigurationBuilder : IConfigurationBuilder
8 | {
9 | private readonly IConfigurationBuilder _configurationBuilder;
10 |
11 | private readonly RemoteConfigurationOptions _remoteConfigurationOptions;
12 |
13 | public IDictionary Properties => _configurationBuilder.Properties;
14 |
15 | public IList Sources => _configurationBuilder.Sources;
16 |
17 | public RemoteConfigurationBuilder(IConfigurationBuilder configurationBuilder, RemoteConfigurationOptions remoteConfigurationOptions)
18 | {
19 | _configurationBuilder = configurationBuilder ?? throw new ArgumentNullException(nameof(configurationBuilder));
20 | _remoteConfigurationOptions = remoteConfigurationOptions ?? throw new ArgumentNullException(nameof(remoteConfigurationOptions));
21 | }
22 |
23 | public IConfigurationBuilder Add(IConfigurationSource source)
24 | {
25 | return _configurationBuilder.Add(source);
26 | }
27 |
28 | public IConfigurationRoot Build()
29 | {
30 | foreach (var configuration in _remoteConfigurationOptions.Configurations)
31 | {
32 | var source = new RemoteConfigurationSource
33 | {
34 | ConfigurationServiceUri = _remoteConfigurationOptions.ServiceUri,
35 | HttpMessageHandler = _remoteConfigurationOptions.HttpMessageHandler,
36 | RequestTimeout = _remoteConfigurationOptions.RequestTimeout,
37 | LoggerFactory = _remoteConfigurationOptions.LoggerFactory,
38 |
39 | ConfigurationName = configuration.ConfigurationName,
40 | Optional = configuration.Optional,
41 | ReloadOnChange = configuration.ReloadOnChange,
42 | Parser = configuration.Parser,
43 |
44 | CreateSubscriber = _remoteConfigurationOptions.CreateSubscriber
45 | };
46 |
47 | Add(source);
48 | }
49 |
50 | return _configurationBuilder.Build();
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace ConfigurationService.Client
5 | {
6 | [Serializable]
7 | public class RemoteConfigurationException : Exception
8 | {
9 | public RemoteConfigurationException()
10 | {
11 | }
12 |
13 | public RemoteConfigurationException(string name)
14 | : base($"{name} cannot be NULL or empty.")
15 | {
16 | }
17 |
18 | public RemoteConfigurationException(string message, Exception inner)
19 | : base(message, inner)
20 | {
21 | }
22 |
23 | protected RemoteConfigurationException(SerializationInfo info, StreamingContext context)
24 | : base(info, context)
25 | {
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Configuration;
3 |
4 | namespace ConfigurationService.Client
5 | {
6 | public static class RemoteConfigurationExtensions
7 | {
8 | ///
9 | /// Adds a remote configuration source.
10 | ///
11 | /// The to add to.
12 | /// Configures the source.
13 | ///
14 | public static IConfigurationBuilder AddRemoteConfiguration(this IConfigurationBuilder builder, Action configure)
15 | {
16 | if (builder == null)
17 | {
18 | throw new ArgumentNullException(nameof(builder));
19 | }
20 |
21 | if (configure == null)
22 | {
23 | throw new ArgumentNullException(nameof(configure));
24 | }
25 |
26 | var options = new RemoteConfigurationOptions();
27 | configure(options);
28 |
29 | var remoteBuilder = new RemoteConfigurationBuilder(builder, options);
30 | return remoteBuilder;
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net.Http;
4 | using ConfigurationService.Client.Subscribers.Nats;
5 | using ConfigurationService.Client.Subscribers.RabbitMq;
6 | using ConfigurationService.Client.Subscribers.Redis;
7 | using Microsoft.Extensions.Logging;
8 | using NATS.Client;
9 | using RedisOptions = StackExchange.Redis.ConfigurationOptions;
10 | using NatsOptions = NATS.Client.Options;
11 |
12 | namespace ConfigurationService.Client
13 | {
14 | public class RemoteConfigurationOptions
15 | {
16 | internal IList Configurations { get; } = new List();
17 |
18 | internal Func CreateSubscriber { get; set; }
19 |
20 | ///
21 | /// Configuration service endpoint.
22 | ///
23 | public string ServiceUri { get; set; }
24 |
25 | ///
26 | /// The for the .
27 | ///
28 | public HttpMessageHandler HttpMessageHandler { get; set; }
29 |
30 | ///
31 | /// The timeout for the request to the configuration server.
32 | ///
33 | public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(60);
34 |
35 | ///
36 | /// The type used to configure the logging system and create instances of
37 | ///
38 | public ILoggerFactory LoggerFactory { get; set; }
39 |
40 | ///
41 | /// Adds an individual configuration file.
42 | ///
43 | /// Configures the options for the configuration file.
44 | public void AddConfiguration(Action configure)
45 | {
46 | var configurationOptions = new ConfigurationOptions();
47 | configure(configurationOptions);
48 |
49 | Configurations.Add(configurationOptions);
50 | }
51 |
52 | ///
53 | /// Adds the type used to configure the logging system and create instances of
54 | ///
55 | ///
56 | public void AddLoggerFactory(ILoggerFactory loggerFactory)
57 | {
58 | LoggerFactory = loggerFactory;
59 | }
60 |
61 | ///
62 | /// Adds a custom subscriber.
63 | ///
64 | /// The delegate used to create the custom implementation of .
65 | public void AddSubscriber(Func subscriberFactory)
66 | {
67 | if (CreateSubscriber != null)
68 | {
69 | throw new InvalidOperationException("A subscriber has already been configured.");
70 | }
71 |
72 | CreateSubscriber = subscriberFactory ?? throw new ArgumentNullException(nameof(subscriberFactory));
73 | }
74 |
75 | ///
76 | /// Adds RabbitMQ as the configuration subscriber.
77 | ///
78 | /// Configure options for the RabbitMQ subscriber.
79 | public void AddRabbitMqSubscriber(Action configure)
80 | {
81 | if (configure == null)
82 | {
83 | throw new ArgumentNullException(nameof(configure));
84 | }
85 |
86 | if (CreateSubscriber != null)
87 | {
88 | throw new InvalidOperationException("A subscriber has already been configured.");
89 | }
90 |
91 | var options = new RabbitMqOptions();
92 | configure(options);
93 |
94 | CreateSubscriber = () => new RabbitMqSubscriber(options);
95 | }
96 |
97 | ///
98 | /// Adds Redis as the configuration subscriber.
99 | ///
100 | /// Configure options for the Redis multiplexer.
101 | public void AddRedisSubscriber(Action configure)
102 | {
103 | if (configure == null)
104 | {
105 | throw new ArgumentNullException(nameof(configure));
106 | }
107 |
108 | if (CreateSubscriber != null)
109 | {
110 | throw new InvalidOperationException("A subscriber has already been configured.");
111 | }
112 |
113 | var options = new RedisOptions();
114 | configure(options);
115 |
116 | CreateSubscriber = () => new RedisSubscriber(options);
117 | }
118 |
119 | ///
120 | /// Adds Redis as the configuration subscriber.
121 | ///
122 | /// The string configuration for the Redis multiplexer.
123 | public void AddRedisSubscriber(string configuration)
124 | {
125 | if (configuration == null)
126 | {
127 | throw new ArgumentNullException(nameof(configuration));
128 | }
129 |
130 | if (CreateSubscriber != null)
131 | {
132 | throw new InvalidOperationException("A subscriber has already been configured.");
133 | }
134 |
135 | var options = RedisOptions.Parse(configuration);
136 |
137 | CreateSubscriber = () => new RedisSubscriber(options);
138 | }
139 |
140 | ///
141 | /// Adds NATS as the configuration subscriber.
142 | ///
143 | /// Configure options for the NATS connection.
144 | public void AddNatsSubscriber(Action configure)
145 | {
146 | if (configure == null)
147 | {
148 | throw new ArgumentNullException(nameof(configure));
149 | }
150 |
151 | if (CreateSubscriber != null)
152 | {
153 | throw new InvalidOperationException("A subscriber has already been configured.");
154 | }
155 |
156 | var options = ConnectionFactory.GetDefaultOptions();
157 | configure(options);
158 |
159 | CreateSubscriber = () => new NatsSubscriber(options);
160 | }
161 | }
162 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Security.Cryptography;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using ConfigurationService.Client.Parsers;
10 | using Microsoft.Extensions.Configuration;
11 | using Microsoft.Extensions.Logging;
12 | using Microsoft.Extensions.Logging.Abstractions;
13 |
14 | namespace ConfigurationService.Client
15 | {
16 | internal class RemoteConfigurationProvider : ConfigurationProvider, IDisposable
17 | {
18 | private readonly ILogger _logger;
19 |
20 | private readonly RemoteConfigurationSource _source;
21 | private readonly Lazy _httpClient;
22 | private readonly IConfigurationParser _parser;
23 | private bool _disposed;
24 |
25 | private string Hash { get; set; }
26 |
27 | private HttpClient HttpClient => _httpClient.Value;
28 |
29 | public RemoteConfigurationProvider(RemoteConfigurationSource source)
30 | {
31 | _source = source ?? throw new ArgumentNullException(nameof(source));
32 |
33 | if (string.IsNullOrWhiteSpace(source.ConfigurationName))
34 | {
35 | throw new RemoteConfigurationException(nameof(source.ConfigurationName));
36 | }
37 |
38 | if (string.IsNullOrWhiteSpace(source.ConfigurationServiceUri))
39 | {
40 | throw new RemoteConfigurationException(nameof(source.ConfigurationServiceUri));
41 | }
42 |
43 | Logger.LoggerFactory = source.LoggerFactory ?? new NullLoggerFactory();
44 |
45 | _logger = Logger.CreateLogger();
46 |
47 | _logger.LogInformation("Initializing remote configuration source for configuration '{ConfigurationName}'", source.ConfigurationName);
48 |
49 | _httpClient = new Lazy(CreateHttpClient);
50 |
51 | _parser = source.Parser;
52 |
53 | if (_parser == null)
54 | {
55 | var extension = Path.GetExtension(source.ConfigurationName).ToLower();
56 |
57 | _logger.LogInformation("A file parser was not specified. Attempting to resolve parser from file extension '{Extension}'", extension);
58 |
59 | switch (extension)
60 | {
61 | case ".ini":
62 | _parser = new IniConfigurationFileParser();
63 | break;
64 | case ".xml":
65 | _parser = new XmlConfigurationFileParser();
66 | break;
67 | case ".yaml":
68 | _parser = new YamlConfigurationFileParser();
69 | break;
70 | default:
71 | _parser = new JsonConfigurationFileParser();
72 | break;
73 | }
74 | }
75 |
76 | _logger.LogInformation("Using parser {Name}", _parser.GetType().Name);
77 |
78 | if (source.ReloadOnChange)
79 | {
80 | if (source.CreateSubscriber == null)
81 | {
82 | _logger.LogWarning("ReloadOnChange is enabled but a subscriber has not been configured");
83 | return;
84 | }
85 |
86 | var subscriber = source.CreateSubscriber();
87 |
88 | _logger.LogInformation("Initializing remote configuration {Name} subscriber for configuration '{ConfigurationName}'",
89 | subscriber.Name, source.ConfigurationName);
90 |
91 | subscriber.Initialize();
92 |
93 | subscriber.Subscribe(source.ConfigurationName, message =>
94 | {
95 | _logger.LogInformation("Received remote configuration change subscription for configuration '{ConfigurationName}' with hash {Message}",
96 | source.ConfigurationName, message);
97 |
98 | _logger.LogInformation("Current hash is {Hash}", Hash);
99 |
100 | if (message != null && message.Equals(Hash, StringComparison.OrdinalIgnoreCase))
101 | {
102 | _logger.LogInformation("Configuration '{ConfigurationName}' current hash {Hash} matches new hash",
103 | source.ConfigurationName, Hash);
104 |
105 | _logger.LogInformation("Configuration will not be updated");
106 |
107 | return;
108 | }
109 |
110 | Load();
111 | OnReload();
112 | });
113 | }
114 | }
115 |
116 | public override void Load() => LoadAsync().ContinueWith(task =>
117 | {
118 | if (task.IsFaulted && task.Exception != null)
119 | {
120 | var ex = task.Exception.Flatten();
121 | _logger.LogError(ex, "Failed to load remote configuration provider");
122 | throw ex;
123 | }
124 | }).GetAwaiter().GetResult();
125 |
126 | public void Dispose()
127 | {
128 | Dispose(true);
129 | GC.SuppressFinalize(this);
130 | }
131 |
132 | protected virtual void Dispose(bool disposing)
133 | {
134 | if (_disposed)
135 | {
136 | return;
137 | }
138 |
139 | if (disposing && _httpClient?.IsValueCreated == true)
140 | {
141 | _httpClient.Value.Dispose();
142 | }
143 |
144 | _disposed = true;
145 | }
146 |
147 | private HttpClient CreateHttpClient()
148 | {
149 | var handler = _source.HttpMessageHandler ?? new HttpClientHandler();
150 | var client = new HttpClient(handler, true)
151 | {
152 | BaseAddress = new Uri(_source.ConfigurationServiceUri),
153 | Timeout = _source.RequestTimeout
154 | };
155 |
156 | return client;
157 | }
158 |
159 | private async Task LoadAsync()
160 | {
161 | Data = await RequestConfigurationAsync().ConfigureAwait(false);
162 | }
163 |
164 | private async Task> RequestConfigurationAsync()
165 | {
166 | var encodedConfigurationName = WebUtility.UrlEncode(_source.ConfigurationName);
167 |
168 | _logger.LogInformation("Requesting remote configuration {ConfigurationName} from {BaseAddress}",
169 | _source.ConfigurationName, HttpClient.BaseAddress);
170 |
171 | try
172 | {
173 | using (var response = await HttpClient.GetAsync(encodedConfigurationName).ConfigureAwait(false))
174 | {
175 | _logger.LogInformation("Received response status code {StatusCode} from endpoint for configuration '{ConfigurationName}'",
176 | response.StatusCode, _source.ConfigurationName);
177 |
178 | if (response.IsSuccessStatusCode)
179 | {
180 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
181 | {
182 | _logger.LogInformation("Parsing remote configuration response stream " +
183 | "({Length:N0} bytes) for configuration '{ConfigurationName}'",
184 | stream.Length, _source.ConfigurationName);
185 |
186 | Hash = ComputeHash(stream);
187 | _logger.LogInformation("Computed hash for Configuration '{ConfigurationName}' is {Hash}",
188 | _source.ConfigurationName, Hash);
189 |
190 | stream.Position = 0;
191 | var data = _parser.Parse(stream);
192 |
193 | _logger.LogInformation("Configuration updated for '{ConfigurationName}'", _source.ConfigurationName);
194 |
195 | return data;
196 | }
197 | }
198 |
199 | if (!_source.Optional)
200 | {
201 | throw new HttpRequestException($"Error calling remote configuration endpoint: {response.StatusCode} - {response.ReasonPhrase}");
202 | }
203 | }
204 | }
205 | catch (Exception)
206 | {
207 | if (!_source.Optional)
208 | {
209 | throw;
210 | }
211 | }
212 |
213 | return new Dictionary(StringComparer.OrdinalIgnoreCase);
214 | }
215 |
216 | private string ComputeHash(Stream stream)
217 | {
218 | using (var hash = SHA1.Create())
219 | {
220 | var hashBytes = hash.ComputeHash(stream);
221 |
222 | var sb = new StringBuilder();
223 | foreach (var b in hashBytes)
224 | {
225 | sb.Append(b.ToString("X2"));
226 | }
227 | return sb.ToString();
228 | }
229 | }
230 | }
231 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/RemoteConfigurationSource.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace ConfigurationService.Client
7 | {
8 | ///
9 | /// Represents a remote file as an .
10 | ///
11 | internal class RemoteConfigurationSource : IConfigurationSource
12 | {
13 | ///
14 | /// Configuration service endpoint.
15 | ///
16 | public string ConfigurationServiceUri { get; set; }
17 |
18 | ///
19 | /// Name or path of the configuration file relative to the configuration provider path.
20 | ///
21 | public string ConfigurationName { get; set; }
22 |
23 | ///
24 | /// Determines if loading the file is optional. Defaults to false>.
25 | ///
26 | public bool Optional { get; set; }
27 |
28 | ///
29 | /// Determines whether the source will be loaded if the underlying file changes. Defaults to false.
30 | ///
31 | public bool ReloadOnChange { get; set; }
32 |
33 | ///
34 | /// The for the .
35 | ///
36 | public HttpMessageHandler HttpMessageHandler { get; set; }
37 |
38 | ///
39 | /// The timeout for the request to the configuration server.
40 | ///
41 | public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(60);
42 |
43 | ///
44 | /// The type of used to parse the remote configuration file.
45 | ///
46 | public IConfigurationParser Parser { get; set; }
47 |
48 | ///
49 | /// Delegate to create the type of used to subscribe to published configuration messages.
50 | ///
51 | public Func CreateSubscriber { get; set; }
52 |
53 | ///
54 | /// The type used to configure the logging system and create instances of
55 | ///
56 | public ILoggerFactory LoggerFactory { get; set; }
57 |
58 | public IConfigurationProvider Build(IConfigurationBuilder builder)
59 | {
60 | return new RemoteConfigurationProvider(this);
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Subscribers/Nats/NatsSubscriber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Logging;
3 | using NATS.Client;
4 |
5 | namespace ConfigurationService.Client.Subscribers.Nats
6 | {
7 | public class NatsSubscriber : ISubscriber
8 | {
9 | private readonly ILogger _logger;
10 |
11 | private readonly Options _options;
12 |
13 | private static IConnection _connection;
14 |
15 | public string Name => "NATS";
16 |
17 | public NatsSubscriber(Options options)
18 | {
19 | _logger = Logger.CreateLogger();
20 |
21 | _options = options ?? throw new ArgumentNullException(nameof(options));
22 | }
23 |
24 | public void Initialize()
25 | {
26 | _options.AsyncErrorEventHandler += (sender, args) => { _logger.LogError("NATS replied with an error message: {Message}", args.Error); };
27 |
28 | _options.ClosedEventHandler += (sender, args) => { _logger.LogError(args.Error, "NATS connection was closed"); };
29 |
30 | _options.DisconnectedEventHandler += (sender, args) => { _logger.LogError(args.Error, "NATS connection was disconnected"); };
31 |
32 | _options.ReconnectedEventHandler += (sender, args) => { _logger.LogInformation("NATS connection was restored"); };
33 |
34 | var connectionFactory = new ConnectionFactory();
35 | _connection = connectionFactory.CreateConnection(_options);
36 | }
37 |
38 | public void Subscribe(string topic, Action handler)
39 | {
40 | _logger.LogInformation("Subscribing to NATS subject '{Subject}'", topic);
41 |
42 | _connection.SubscribeAsync(topic, (sender, args) =>
43 | {
44 | _logger.LogInformation("Received subscription on NATS subject '{Subject}'", topic);
45 |
46 | var message = args.Message.ToString();
47 |
48 | handler(message);
49 | });
50 |
51 | _logger.LogInformation("Subscribed to NATS subject '{Subject}'", topic);
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Subscribers/RabbitMq/RabbitMqOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ConfigurationService.Client.Subscribers.RabbitMq
2 | {
3 | public class RabbitMqOptions
4 | {
5 | /// The host to connect to.
6 | /// Defaults to "localhost".
7 | public string HostName { get; set; } = "localhost";
8 |
9 | ///
10 | /// Virtual host to access during this connection.
11 | /// Defaults to "/".
12 | ///
13 | public string VirtualHost { get; set; } = "/";
14 |
15 | ///
16 | /// Username to use when authenticating to the server.
17 | /// Defaults to "guest".
18 | ///
19 | public string UserName { get; set; } = "guest";
20 |
21 | ///
22 | /// Password to use when authenticating to the server.
23 | /// Defaults to "guest".
24 | ///
25 | public string Password { get; set; } = "guest";
26 |
27 | ///
28 | /// Name of the fanout exchange.
29 | /// Defaults to "configuration-service"
30 | ///
31 | public string ExchangeName { get; set; } = "configuration-service";
32 | }
33 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Subscribers/RabbitMq/RabbitMqSubscriber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using Microsoft.Extensions.Logging;
4 | using RabbitMQ.Client;
5 | using RabbitMQ.Client.Events;
6 |
7 | namespace ConfigurationService.Client.Subscribers.RabbitMq
8 | {
9 | public class RabbitMqSubscriber : ISubscriber
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly RabbitMqOptions _options;
14 | private string _exchangeName;
15 | private static IModel _channel;
16 |
17 | public string Name => "RabbitMQ";
18 |
19 | public RabbitMqSubscriber(RabbitMqOptions options)
20 | {
21 | _logger = Logger.CreateLogger();
22 |
23 | _options = options ?? throw new ArgumentNullException(nameof(options));
24 | }
25 |
26 | public void Initialize()
27 | {
28 | var factory = new ConnectionFactory
29 | {
30 | HostName = _options.HostName,
31 | VirtualHost = _options.VirtualHost,
32 | UserName = _options.UserName,
33 | Password = _options.Password
34 | };
35 |
36 | _exchangeName = _options.ExchangeName;
37 |
38 | var connection = factory.CreateConnection();
39 | _channel = connection.CreateModel();
40 |
41 | connection.CallbackException += (sender, args) => { _logger.LogError(args.Exception, "RabbitMQ callback exception"); };
42 |
43 | connection.ConnectionBlocked += (sender, args) => { _logger.LogError("RabbitMQ connection is blocked. Reason: {Reason}", args.Reason); };
44 |
45 | connection.ConnectionShutdown += (sender, args) => { _logger.LogError("RabbitMQ connection was shut down. Reason: {ReplyText}", args.ReplyText); };
46 |
47 | connection.ConnectionUnblocked += (sender, args) => { _logger.LogInformation("RabbitMQ connection was unblocked"); };
48 |
49 | _channel.ExchangeDeclare(_exchangeName, ExchangeType.Fanout);
50 | }
51 |
52 | public void Subscribe(string topic, Action handler)
53 | {
54 | _logger.LogInformation("Binding to RabbitMQ queue with routing key '{RoutingKey}'", topic);
55 |
56 | var queueName = _channel.QueueDeclare().QueueName;
57 | _channel.QueueBind(queueName, _exchangeName, topic);
58 |
59 | var consumer = new EventingBasicConsumer(_channel);
60 |
61 | consumer.Received += (model, args) =>
62 | {
63 | _logger.LogInformation("Received message with routing key '{RoutingKey}'", args.RoutingKey);
64 |
65 | var body = args.Body.ToArray();
66 | var message = Encoding.UTF8.GetString(body);
67 |
68 | handler(message);
69 | };
70 |
71 | var consumerTag = _channel.BasicConsume(queueName, true, consumer);
72 |
73 | _logger.LogInformation("Consuming RabbitMQ queue {QueueName} for consumer '{ConsumerTag}'", queueName, consumerTag);
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Client/Subscribers/Redis/RedisSubscriber.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Microsoft.Extensions.Logging;
4 | using StackExchange.Redis;
5 | using RedisOptions = StackExchange.Redis.ConfigurationOptions;
6 |
7 | namespace ConfigurationService.Client.Subscribers.Redis
8 | {
9 | public class RedisSubscriber : ISubscriber
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly RedisOptions _options;
14 | private static ConnectionMultiplexer _connection;
15 |
16 | public string Name => "Redis";
17 |
18 | public RedisSubscriber(string configuration)
19 | {
20 | _logger = Logger.CreateLogger();
21 |
22 | if (configuration == null)
23 | {
24 | throw new ArgumentNullException(nameof(configuration));
25 | }
26 |
27 | _options = RedisOptions.Parse(configuration);
28 |
29 | }
30 |
31 | public RedisSubscriber(RedisOptions configurationOptions)
32 | {
33 | _logger = Logger.CreateLogger();
34 |
35 | _options = configurationOptions ?? throw new ArgumentNullException(nameof(configurationOptions));
36 | }
37 |
38 | public void Initialize()
39 | {
40 | using (var writer = new StringWriter())
41 | {
42 | _connection = ConnectionMultiplexer.Connect(_options, writer);
43 |
44 | _logger.LogTrace("Redis subscriber connected with log:\r\n{Log}", writer);
45 | }
46 |
47 | _connection.ErrorMessage += (sender, args) => { _logger.LogError("Redis replied with an error message: {Message}", args.Message); };
48 |
49 | _connection.ConnectionFailed += (sender, args) => { _logger.LogError(args.Exception, "Redis connection failed"); };
50 |
51 | _connection.ConnectionRestored += (sender, args) => { _logger.LogInformation("Redis connection restored"); };
52 | }
53 |
54 | public void Subscribe(string topic, Action handler)
55 | {
56 | _logger.LogInformation("Subscribing to Redis channel '{Channel}'", topic);
57 |
58 | var subscriber = _connection.GetSubscriber();
59 |
60 | subscriber.Subscribe(topic, (redisChannel, value) =>
61 | {
62 | _logger.LogInformation("Received subscription on Redis channel '{Channel}'", topic);
63 |
64 | handler(value);
65 | });
66 |
67 | var endpoint = subscriber.SubscribedEndpoint(topic);
68 | _logger.LogInformation("Subscribed to Redis endpoint {Endpoint} for channel '{Channel}'", endpoint, topic);
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ConfigurationEndpointRouteBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Text;
5 | using System.Text.Json;
6 | using ConfigurationService.Hosting.Providers;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.Routing;
10 | using Microsoft.Extensions.DependencyInjection;
11 |
12 | namespace ConfigurationService.Hosting;
13 |
14 | public static class ConfigurationEndpointRouteBuilderExtensions
15 | {
16 | public static IEndpointConventionBuilder MapConfigurationService(this IEndpointRouteBuilder endpoints, string pattern = "/configuration")
17 | {
18 | if (endpoints == null)
19 | {
20 | throw new ArgumentNullException(nameof(endpoints));
21 | }
22 |
23 | if (pattern == null)
24 | {
25 | throw new ArgumentNullException(nameof(pattern));
26 | }
27 |
28 | var conventionBuilders = new List();
29 |
30 | var listConfigurationBuilder = endpoints.RegisterListRoute(pattern);
31 | conventionBuilders.Add(listConfigurationBuilder);
32 |
33 | var fileConfigurationBuilder = endpoints.RegisterFileRoute(pattern);
34 | conventionBuilders.Add(fileConfigurationBuilder);
35 |
36 | return new CompositeEndpointConventionBuilder(conventionBuilders);
37 | }
38 |
39 | private static IEndpointConventionBuilder RegisterListRoute(this IEndpointRouteBuilder endpointRouteBuilder, string pattern)
40 | {
41 | var provider = endpointRouteBuilder.ServiceProvider.GetService();
42 |
43 | return endpointRouteBuilder.MapGet(pattern, async context =>
44 | {
45 | var files = await provider.ListPaths();
46 |
47 | context.Response.OnStarting(async () =>
48 | {
49 | await JsonSerializer.SerializeAsync(context.Response.Body, files);
50 | });
51 |
52 | context.Response.ContentType = "application/json; charset=UTF-8";
53 | await context.Response.Body.FlushAsync();
54 | });
55 | }
56 |
57 | private static IEndpointConventionBuilder RegisterFileRoute(this IEndpointRouteBuilder endpointRouteBuilder, string pattern)
58 | {
59 | var provider = endpointRouteBuilder.ServiceProvider.GetService();
60 |
61 | return endpointRouteBuilder.MapGet(pattern + "/{name}", async context =>
62 | {
63 | var name = context.GetRouteValue("name")?.ToString();
64 | name = WebUtility.UrlDecode(name);
65 |
66 | var bytes = await provider.GetConfiguration(name);
67 |
68 | if (bytes == null)
69 | {
70 | context.Response.StatusCode = 404;
71 | return;
72 | }
73 |
74 | var fileContent = Encoding.UTF8.GetString(bytes);
75 |
76 | await context.Response.WriteAsync(fileContent);
77 | await context.Response.Body.FlushAsync();
78 | });
79 | }
80 |
81 | private sealed class CompositeEndpointConventionBuilder : IEndpointConventionBuilder
82 | {
83 | private readonly List _endpointConventionBuilders;
84 |
85 | public CompositeEndpointConventionBuilder(List endpointConventionBuilders)
86 | {
87 | _endpointConventionBuilders = endpointConventionBuilders;
88 | }
89 |
90 | public void Add(Action convention)
91 | {
92 | foreach (var endpointConventionBuilder in _endpointConventionBuilders)
93 | {
94 | endpointConventionBuilder.Add(convention);
95 | }
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ConfigurationService.Hosting.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Contains the hosted service, storage providers and publisher components for Configuration Service.
6 | Configuration Service is a distributed configuration service for .NET.
7 |
8 | net6.0
9 | 1.0.2
10 | James Pratt
11 | ConfigurationService.Hosting
12 | ConfigurationService.Hosting
13 |
14 |
15 |
16 | true
17 | $(NoWarn);1591
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ConfigurationService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using ConfigurationService.Hosting.Providers;
6 | using ConfigurationService.Hosting.Publishers;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace ConfigurationService.Hosting;
10 |
11 | public class ConfigurationService : IConfigurationService
12 | {
13 | private readonly ILogger _logger;
14 |
15 | private readonly IProvider _provider;
16 | private readonly IPublisher _publisher;
17 |
18 | public ConfigurationService(ILogger logger, IProvider provider, IPublisher publisher = null)
19 | {
20 | _logger = logger;
21 | _provider = provider;
22 | _publisher = publisher;
23 |
24 | if (_publisher == null)
25 | {
26 | _logger.LogInformation("A publisher has not been configured");
27 | }
28 | }
29 |
30 | public async Task Initialize(CancellationToken cancellationToken = default)
31 | {
32 | _logger.LogInformation("Initializing {Name} configuration provider...", _provider.Name);
33 |
34 | _provider.Initialize();
35 |
36 | if (_publisher != null)
37 | {
38 | _logger.LogInformation("Initializing publisher...");
39 | _publisher.Initialize();
40 | }
41 |
42 | var paths = await _provider.ListPaths();
43 |
44 | await PublishChanges(paths);
45 |
46 | await _provider.Watch(OnChange, cancellationToken);
47 |
48 | _logger.LogInformation("{Name} configuration watching for changes", _provider.Name);
49 | }
50 |
51 | public async Task OnChange(IEnumerable paths)
52 | {
53 | _logger.LogInformation("Changes were detected on the remote {Name} configuration provider", _provider.Name);
54 |
55 | paths = paths.ToList();
56 |
57 | if (paths.Any())
58 | {
59 | await PublishChanges(paths);
60 | }
61 | }
62 |
63 | public async Task PublishChanges(IEnumerable paths)
64 | {
65 | if (_publisher == null)
66 | {
67 | return;
68 | }
69 |
70 | _logger.LogInformation("Publishing changes...");
71 |
72 | foreach (var path in paths)
73 | {
74 | var hash = await _provider.GetHash(path);
75 | await _publisher.Publish(path, hash);
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ConfigurationServiceBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace ConfigurationService.Hosting;
5 |
6 | public class ConfigurationServiceBuilder : IConfigurationServiceBuilder
7 | {
8 | public IServiceCollection Services { get; }
9 |
10 | public ConfigurationServiceBuilder(IServiceCollection services)
11 | {
12 | Services = services ?? throw new ArgumentNullException(nameof(services));
13 | }
14 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ConfigurationServiceBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using ConfigurationService.Hosting.Providers;
3 | using ConfigurationService.Hosting.Providers.FileSystem;
4 | using ConfigurationService.Hosting.Providers.Git;
5 | using ConfigurationService.Hosting.Providers.Vault;
6 | using ConfigurationService.Hosting.Publishers;
7 | using ConfigurationService.Hosting.Publishers.Nats;
8 | using ConfigurationService.Hosting.Publishers.RabbitMq;
9 | using ConfigurationService.Hosting.Publishers.Redis;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using NATS.Client;
12 | using StackExchange.Redis;
13 |
14 | namespace ConfigurationService.Hosting;
15 |
16 | public static class ConfigurationServiceBuilderExtensions
17 | {
18 | ///
19 | /// Adds services for configuration hosting to the specified .
20 | ///
21 | /// The to add services to.
22 | /// An that can be used to further configure the
23 | /// ConfigurationService services.
24 | public static IConfigurationServiceBuilder AddConfigurationService(this IServiceCollection services)
25 | {
26 | if (services == null)
27 | {
28 | throw new ArgumentNullException(nameof(services));
29 | }
30 |
31 | services.AddHostedService();
32 | services.AddSingleton();
33 |
34 | return new ConfigurationServiceBuilder(services);
35 | }
36 |
37 | ///
38 | /// Add Git as the storage provider backend.
39 | ///
40 | /// The to add services to.
41 | /// Configure git provider options.
42 | /// An that can be used to further configure the
43 | /// ConfigurationService services.
44 | public static IConfigurationServiceBuilder AddGitProvider(this IConfigurationServiceBuilder builder, Action configure)
45 | {
46 | if (builder == null)
47 | {
48 | throw new ArgumentNullException(nameof(builder));
49 | }
50 |
51 | if (configure == null)
52 | {
53 | throw new ArgumentNullException(nameof(configure));
54 | }
55 |
56 | var options = new GitProviderOptions();
57 | configure(options);
58 |
59 | builder.Services.AddSingleton(options);
60 | builder.Services.AddSingleton();
61 |
62 | return builder;
63 | }
64 |
65 | ///
66 | /// Add file system as the storage provider backend.
67 | ///
68 | /// The to add services to.
69 | /// Configure file system provider options.
70 | /// An that can be used to further configure the
71 | /// ConfigurationService services.
72 | public static IConfigurationServiceBuilder AddFileSystemProvider(this IConfigurationServiceBuilder builder, Action configure)
73 | {
74 | if (builder == null)
75 | {
76 | throw new ArgumentNullException(nameof(builder));
77 | }
78 |
79 | if (configure == null)
80 | {
81 | throw new ArgumentNullException(nameof(configure));
82 | }
83 |
84 | var options = new FileSystemProviderOptions();
85 | configure(options);
86 |
87 | builder.Services.AddSingleton(options);
88 | builder.Services.AddSingleton();
89 |
90 | return builder;
91 | }
92 |
93 | ///
94 | /// Add Vault as the storage provider backend.
95 | ///
96 | /// The to add services to.
97 | /// Configure Vault provider options.
98 | /// An that can be used to further configure the
99 | /// ConfigurationService services.
100 | public static IConfigurationServiceBuilder AddVaultProvider(this IConfigurationServiceBuilder builder, Action configure)
101 | {
102 | if (builder == null)
103 | {
104 | throw new ArgumentNullException(nameof(builder));
105 | }
106 |
107 | if (configure == null)
108 | {
109 | throw new ArgumentNullException(nameof(configure));
110 | }
111 |
112 | var options = new VaultProviderOptions();
113 | configure(options);
114 |
115 | builder.Services.AddSingleton(options);
116 | builder.Services.AddSingleton();
117 |
118 | return builder;
119 | }
120 |
121 | ///
122 | /// Adds a custom storage provider backend.
123 | ///
124 | /// The to add services to.
125 | /// The custom implementation of .
126 | /// An that can be used to further configure the
127 | /// ConfigurationService services.
128 | public static IConfigurationServiceBuilder AddProvider(this IConfigurationServiceBuilder builder, IProvider provider)
129 | {
130 | if (builder == null)
131 | {
132 | throw new ArgumentNullException(nameof(builder));
133 | }
134 |
135 | if (provider == null)
136 | {
137 | throw new ArgumentNullException(nameof(provider));
138 | }
139 |
140 | builder.Services.AddSingleton(provider);
141 |
142 | return builder;
143 | }
144 |
145 | ///
146 | /// Adds RabbitMQ as the configuration publisher.
147 | ///
148 | /// The to add services to.
149 | /// Configure options for the RabbitMQ publisher.
150 | /// An that can be used to further configure the
151 | /// ConfigurationService services.
152 | public static IConfigurationServiceBuilder AddRabbitMqPublisher(this IConfigurationServiceBuilder builder, Action configure)
153 | {
154 | if (builder == null)
155 | {
156 | throw new ArgumentNullException(nameof(builder));
157 | }
158 |
159 | if (configure == null)
160 | {
161 | throw new ArgumentNullException(nameof(configure));
162 | }
163 |
164 | var options = new RabbitMqOptions();
165 | configure(options);
166 |
167 | builder.Services.AddSingleton(options);
168 | builder.Services.AddSingleton();
169 |
170 | return builder;
171 | }
172 |
173 | ///
174 | /// Adds Redis as the configuration publisher.
175 | ///
176 | /// The to add services to.
177 | /// Configure options for the Redis multiplexer.
178 | /// An that can be used to further configure the
179 | /// ConfigurationService services.
180 | public static IConfigurationServiceBuilder AddRedisPublisher(this IConfigurationServiceBuilder builder, Action configure)
181 | {
182 | if (builder == null)
183 | {
184 | throw new ArgumentNullException(nameof(builder));
185 | }
186 |
187 | if (configure == null)
188 | {
189 | throw new ArgumentNullException(nameof(configure));
190 | }
191 |
192 | var options = new ConfigurationOptions();
193 | configure(options);
194 |
195 | builder.Services.AddSingleton(options);
196 | builder.Services.AddSingleton();
197 |
198 | return builder;
199 | }
200 |
201 | ///
202 | /// Adds Redis as the configuration publisher.
203 | ///
204 | /// The to add services to.
205 | /// The string configuration for the Redis multiplexer.
206 | /// An that can be used to further configure the
207 | /// ConfigurationService services.
208 | public static IConfigurationServiceBuilder AddRedisPublisher(this IConfigurationServiceBuilder builder, string configuration)
209 | {
210 | if (builder == null)
211 | {
212 | throw new ArgumentNullException(nameof(builder));
213 | }
214 |
215 | if (configuration == null)
216 | {
217 | throw new ArgumentNullException(nameof(configuration));
218 | }
219 |
220 | var options = ConfigurationOptions.Parse(configuration);
221 |
222 | builder.Services.AddSingleton(options);
223 | builder.Services.AddSingleton();
224 |
225 | return builder;
226 | }
227 |
228 | ///
229 | /// Adds NATS as the configuration publisher.
230 | ///
231 | /// The to add services to.
232 | /// Configure options for the NATS connection.
233 | /// An that can be used to further configure the
234 | /// ConfigurationService services.
235 | public static IConfigurationServiceBuilder AddNatsPublisher(this IConfigurationServiceBuilder builder, Action configure)
236 | {
237 | if (builder == null)
238 | {
239 | throw new ArgumentNullException(nameof(builder));
240 | }
241 |
242 | if (configure == null)
243 | {
244 | throw new ArgumentNullException(nameof(configure));
245 | }
246 |
247 | var options = ConnectionFactory.GetDefaultOptions();
248 | configure(options);
249 |
250 | builder.Services.AddSingleton(options);
251 | builder.Services.AddSingleton();
252 |
253 | return builder;
254 | }
255 |
256 | ///
257 | /// Adds a custom configuration publisher.
258 | ///
259 | /// The to add services to.
260 | /// The custom implementation of .
261 | /// An that can be used to further configure the
262 | /// ConfigurationService services.
263 | public static IConfigurationServiceBuilder AddPublisher(this IConfigurationServiceBuilder builder, IPublisher publisher)
264 | {
265 | if (builder == null)
266 | {
267 | throw new ArgumentNullException(nameof(builder));
268 | }
269 |
270 | if (publisher == null)
271 | {
272 | throw new ArgumentNullException(nameof(publisher));
273 | }
274 |
275 | builder.Services.AddSingleton(publisher);
276 |
277 | return builder;
278 | }
279 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace ConfigurationService.Hosting.Extensions;
4 |
5 | public static class StringExtensions
6 | {
7 |
8 | public static string NormalizePathSeparators(this string path)
9 | {
10 | return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Hasher.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography;
2 | using System.Text;
3 |
4 | namespace ConfigurationService.Hosting;
5 |
6 | public static class Hasher
7 | {
8 | public static string CreateHash(byte[] bytes)
9 | {
10 | using var hash = SHA1.Create();
11 | var hashBytes = hash.ComputeHash(bytes);
12 |
13 | var sb = new StringBuilder();
14 | foreach (var b in hashBytes)
15 | {
16 | sb.Append(b.ToString("X2"));
17 | }
18 | return sb.ToString();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/HostedConfigurationService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Hosting;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace ConfigurationService.Hosting;
8 |
9 | public class HostedConfigurationService : IHostedService, IDisposable
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly IHostApplicationLifetime _applicationLifetime;
14 | private readonly IConfigurationService _configurationService;
15 |
16 | private Task _executingTask;
17 | private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
18 | private bool _disposed;
19 |
20 | public HostedConfigurationService(ILogger logger, IHostApplicationLifetime applicationLifetime, IConfigurationService configurationService)
21 | {
22 | _logger = logger;
23 | _applicationLifetime = applicationLifetime;
24 | _configurationService = configurationService;
25 | }
26 |
27 | public Task StartAsync(CancellationToken cancellationToken)
28 | {
29 | _logger.LogInformation("Starting Configuration Service");
30 |
31 | _applicationLifetime.ApplicationStarted.Register(OnStarted);
32 | _applicationLifetime.ApplicationStopping.Register(OnStopping);
33 | _applicationLifetime.ApplicationStopped.Register(OnStopped);
34 |
35 | _executingTask = ExecuteAsync(_stoppingCts.Token);
36 |
37 | if (_executingTask.IsCompleted)
38 | {
39 | return _executingTask;
40 | }
41 |
42 | return Task.CompletedTask;
43 | }
44 |
45 | public async Task StopAsync(CancellationToken cancellationToken)
46 | {
47 | if (_executingTask == null)
48 | {
49 | return;
50 | }
51 |
52 | try
53 | {
54 | // Signal cancellation to the executing method
55 | _stoppingCts.Cancel();
56 | }
57 | finally
58 | {
59 | // Wait until the task completes or the stop token triggers
60 | await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
61 | }
62 | }
63 |
64 | public async Task ExecuteAsync(CancellationToken stoppingToken)
65 | {
66 | try
67 | {
68 | await _configurationService.Initialize(stoppingToken);
69 | }
70 | catch (Exception ex)
71 | {
72 | _logger.LogError(ex, "An unhandled exception occurred while attempting to initialize the configuration provider");
73 |
74 | _logger.LogInformation("The application will be terminated");
75 |
76 | await StopAsync(stoppingToken);
77 | _applicationLifetime.StopApplication();
78 | }
79 | }
80 |
81 | public void Dispose()
82 | {
83 | Dispose(true);
84 | GC.SuppressFinalize(this);
85 | }
86 |
87 | protected virtual void Dispose(bool disposing)
88 | {
89 | if (_disposed)
90 | {
91 | return;
92 | }
93 |
94 | if (disposing)
95 | {
96 | _stoppingCts.Cancel();
97 | }
98 |
99 | _disposed = true;
100 | }
101 |
102 | private void OnStarted()
103 | {
104 | _logger.LogInformation("Configuration Service started");
105 | }
106 |
107 | private void OnStopping()
108 | {
109 | _logger.LogInformation("Configuration Service is stopping...");
110 | }
111 |
112 | private void OnStopped()
113 | {
114 | _logger.LogInformation("Configuration Service stopped");
115 | }
116 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/IConfigurationService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace ConfigurationService.Hosting;
6 |
7 | public interface IConfigurationService
8 | {
9 | Task Initialize(CancellationToken cancellationToken = default);
10 |
11 | Task OnChange(IEnumerable paths);
12 |
13 | Task PublishChanges(IEnumerable paths);
14 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/IConfigurationServiceBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace ConfigurationService.Hosting;
4 |
5 | public interface IConfigurationServiceBuilder
6 | {
7 | IServiceCollection Services { get; }
8 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/ProviderOptionNullException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.Serialization;
3 |
4 | namespace ConfigurationService.Hosting;
5 |
6 | [Serializable]
7 | public class ProviderOptionNullException : Exception
8 | {
9 | public ProviderOptionNullException()
10 | {
11 | }
12 |
13 | public ProviderOptionNullException(string name)
14 | : base($"{name} cannot be NULL or empty.")
15 | {
16 | }
17 |
18 | public ProviderOptionNullException(string message, Exception inner)
19 | : base(message, inner)
20 | {
21 | }
22 |
23 | protected ProviderOptionNullException(SerializationInfo info, StreamingContext context)
24 | : base(info, context)
25 | {
26 | }
27 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/FileSystem/FileSystemProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace ConfigurationService.Hosting.Providers.FileSystem;
11 |
12 | public class FileSystemProvider : IProvider
13 | {
14 | private readonly ILogger _logger;
15 |
16 | private readonly FileSystemProviderOptions _providerOptions;
17 | private FileSystemWatcher _fileSystemWatcher;
18 | private Func, Task> _onChange;
19 |
20 | public string Name => "File System";
21 |
22 | public FileSystemProvider(ILogger logger, FileSystemProviderOptions providerOptions)
23 | {
24 | _logger = logger;
25 | _providerOptions = providerOptions;
26 |
27 | if (string.IsNullOrWhiteSpace(_providerOptions.Path))
28 | {
29 | throw new ProviderOptionNullException(nameof(_providerOptions.Path));
30 | }
31 | }
32 |
33 | public Task Watch(Func, Task> onChange, CancellationToken cancellationToken = default)
34 | {
35 | _onChange = onChange;
36 | _fileSystemWatcher.EnableRaisingEvents = true;
37 | return Task.CompletedTask;
38 | }
39 |
40 | public void Initialize()
41 | {
42 | _logger.LogInformation("Initializing {Name} provider with options {@Options}", Name, new
43 | {
44 | _providerOptions.Path,
45 | _providerOptions.SearchPattern,
46 | _providerOptions.IncludeSubdirectories
47 | });
48 |
49 | if (_providerOptions.Username != null && _providerOptions.Password != null)
50 | {
51 | var credentials = new NetworkCredential(_providerOptions.Username, _providerOptions.Password, _providerOptions.Domain);
52 | var uri = new Uri(_providerOptions.Path);
53 | _ = new CredentialCache
54 | {
55 | {new Uri($"{uri.Scheme}://{uri.Host}"), "Basic", credentials}
56 | };
57 | }
58 |
59 | _fileSystemWatcher = new FileSystemWatcher
60 | {
61 | Path = _providerOptions.Path,
62 | Filter = _providerOptions.SearchPattern,
63 | IncludeSubdirectories = _providerOptions.IncludeSubdirectories,
64 | NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
65 | };
66 |
67 | _fileSystemWatcher.Created += FileSystemWatcher_Changed;
68 | _fileSystemWatcher.Changed += FileSystemWatcher_Changed;
69 | }
70 |
71 | public async Task GetConfiguration(string name)
72 | {
73 | string path = Path.Combine(_providerOptions.Path, name);
74 |
75 | if (!File.Exists(path))
76 | {
77 | _logger.LogInformation("File does not exist at {Path}", path);
78 | return null;
79 | }
80 |
81 | return await File.ReadAllBytesAsync(path);
82 | }
83 |
84 | public async Task GetHash(string name)
85 | {
86 | var bytes = await GetConfiguration(name);
87 |
88 | return Hasher.CreateHash(bytes);
89 | }
90 |
91 | public Task> ListPaths()
92 | {
93 | _logger.LogInformation("Listing files at {Path}", _providerOptions.Path);
94 |
95 | var searchOption = _providerOptions.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
96 | var files = Directory.EnumerateFiles(_providerOptions.Path, _providerOptions.SearchPattern ?? "*", searchOption).ToList();
97 | files = files.Select(GetRelativePath).ToList();
98 |
99 | _logger.LogInformation("{Count} files found", files.Count);
100 |
101 | return Task.FromResult>(files);
102 | }
103 |
104 | private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
105 | {
106 | _logger.LogInformation("Detected file change at {FullPath}", e.FullPath);
107 |
108 | var filename = GetRelativePath(e.FullPath);
109 | _onChange(new[] { filename });
110 | }
111 |
112 | private string GetRelativePath(string fullPath)
113 | {
114 | return Path.GetRelativePath(_providerOptions.Path, fullPath);
115 | }
116 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/FileSystem/FileSystemProviderOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ConfigurationService.Hosting.Providers.FileSystem;
2 |
3 | ///
4 | /// Options for .
5 | ///
6 | public class FileSystemProviderOptions
7 | {
8 | ///
9 | /// Path to the configuration files.
10 | ///
11 | public string Path { get; set; }
12 |
13 | ///
14 | /// The search string to use as a filter against the names of files. Defaults to all files ('*').
15 | ///
16 | public string SearchPattern { get; set; }
17 |
18 | ///
19 | /// Includes the current directory and all its subdirectories.
20 | ///
21 | public bool IncludeSubdirectories { get; set; }
22 |
23 | ///
24 | /// Username for authentication.
25 | ///
26 | public string Username { get; set; }
27 |
28 | ///
29 | /// Password for authentication.
30 | ///
31 | public string Password { get; set; }
32 |
33 | ///
34 | /// Domain for authentication.
35 | ///
36 | public string Domain { get; set; }
37 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/Git/GitProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using ConfigurationService.Hosting.Extensions;
8 |
9 | using LibGit2Sharp;
10 | using LibGit2Sharp.Handlers;
11 | using Microsoft.Extensions.Logging;
12 |
13 | namespace ConfigurationService.Hosting.Providers.Git;
14 |
15 | public class GitProvider : IProvider
16 | {
17 | private readonly ILogger _logger;
18 |
19 | private readonly GitProviderOptions _providerOptions;
20 | private CredentialsHandler _credentialsHandler;
21 |
22 | public string Name => "Git";
23 |
24 | public GitProvider(ILogger logger, GitProviderOptions providerOptions)
25 | {
26 | _logger = logger;
27 | _providerOptions = providerOptions;
28 |
29 | if (string.IsNullOrWhiteSpace(_providerOptions.LocalPath))
30 | {
31 | throw new ProviderOptionNullException(nameof(_providerOptions.LocalPath));
32 | }
33 |
34 | if (string.IsNullOrWhiteSpace(_providerOptions.RepositoryUrl))
35 | {
36 | throw new ProviderOptionNullException(nameof(_providerOptions.RepositoryUrl));
37 | }
38 | }
39 |
40 | public async Task Watch(Func, Task> onChange, CancellationToken cancellationToken = default)
41 | {
42 | while (!cancellationToken.IsCancellationRequested)
43 | {
44 | try
45 | {
46 | List files;
47 |
48 | var task = Task.Run(ListChangedFiles, cancellationToken);
49 | // The git fetch operation can sometimes hang. Force to complete after a minute.
50 | if (task.Wait(TimeSpan.FromSeconds(60)))
51 | {
52 | files = task.Result.ToList();
53 | }
54 | else
55 | {
56 | throw new TimeoutException("Attempting to list changed files timed out after 60 seconds.");
57 | }
58 |
59 | if (files.Count > 0)
60 | {
61 | await onChange(files);
62 | }
63 | }
64 | catch (Exception ex)
65 | {
66 | _logger.LogError(ex, "An unhandled exception occurred while attempting to poll for changes");
67 | }
68 |
69 | var delayDate = DateTime.UtcNow.Add(_providerOptions.PollingInterval);
70 |
71 | _logger.LogInformation("Next polling period will begin in {PollingInterval:c} at {DelayDate}",
72 | _providerOptions.PollingInterval, delayDate);
73 |
74 | await Task.Delay(_providerOptions.PollingInterval, cancellationToken);
75 | }
76 | }
77 |
78 | public void Initialize()
79 | {
80 | _logger.LogInformation("Initializing {Name} provider with options {@Options}", Name, new
81 | {
82 | _providerOptions.RepositoryUrl,
83 | _providerOptions.LocalPath,
84 | _providerOptions.Branch,
85 | _providerOptions.PollingInterval,
86 | _providerOptions.SearchPattern
87 | });
88 |
89 | if (Directory.Exists(_providerOptions.LocalPath))
90 | {
91 | _logger.LogInformation("A local repository already exists at {LocalPath}", _providerOptions.LocalPath);
92 |
93 | _logger.LogInformation("Deleting directory {LocalPath}", _providerOptions.LocalPath);
94 |
95 | DeleteDirectory(_providerOptions.LocalPath);
96 | }
97 |
98 | if (!Directory.Exists(_providerOptions.LocalPath))
99 | {
100 | _logger.LogInformation("Creating directory {LocalPath}", _providerOptions.LocalPath);
101 |
102 | Directory.CreateDirectory(_providerOptions.LocalPath);
103 | }
104 |
105 | if (_providerOptions.Username != null && _providerOptions.Password != null)
106 | {
107 | _credentialsHandler = (url, user, cred) => new UsernamePasswordCredentials
108 | {
109 | Username = _providerOptions.Username,
110 | Password = _providerOptions.Password
111 | };
112 | }
113 |
114 | var cloneOptions = new CloneOptions
115 | {
116 | CredentialsProvider = _credentialsHandler,
117 | BranchName = _providerOptions.Branch
118 | };
119 |
120 | _logger.LogInformation("Cloning git repository {RepositoryUrl} to {LocalPath}", _providerOptions.RepositoryUrl, _providerOptions.LocalPath);
121 |
122 | var path = Repository.Clone(_providerOptions.RepositoryUrl, _providerOptions.LocalPath, cloneOptions);
123 |
124 | _logger.LogInformation("Repository cloned to {Path}", path);
125 |
126 | using var repo = new Repository(_providerOptions.LocalPath);
127 | var hash = repo.Head.Tip.Sha.Substring(0, 6);
128 |
129 | _logger.LogInformation("Current HEAD is [{Hash}] '{MessageShort}'", hash, repo.Head.Tip.MessageShort);
130 | }
131 |
132 | public async Task GetConfiguration(string name)
133 | {
134 | string path = Path.Combine(_providerOptions.LocalPath, name);
135 |
136 | if (!File.Exists(path))
137 | {
138 | _logger.LogInformation("File does not exist at {Path}", path);
139 | return null;
140 | }
141 |
142 | return await File.ReadAllBytesAsync(path);
143 | }
144 |
145 | public async Task GetHash(string name)
146 | {
147 | var bytes = await GetConfiguration(name);
148 |
149 | return Hasher.CreateHash(bytes);
150 | }
151 |
152 | public Task> ListPaths()
153 | {
154 | _logger.LogInformation("Listing files at {LocalPath}", _providerOptions.LocalPath);
155 |
156 | IList files = new List();
157 |
158 | using (var repo = new Repository(_providerOptions.LocalPath))
159 | {
160 | _logger.LogInformation("Listing files in repository at {LocalPath}", _providerOptions.LocalPath);
161 |
162 | foreach (var entry in repo.Index)
163 | {
164 | files.Add(entry.Path.NormalizePathSeparators());
165 | }
166 | }
167 |
168 | var localFiles = Directory.EnumerateFiles(_providerOptions.LocalPath, _providerOptions.SearchPattern ?? "*", SearchOption.AllDirectories).ToList();
169 | localFiles = localFiles.Select(GetRelativePath).ToList();
170 |
171 | files = localFiles.Intersect(files).ToList();
172 |
173 | _logger.LogInformation("{Count} files found", files.Count);
174 |
175 | return Task.FromResult>(files);
176 | }
177 |
178 | private async Task> ListChangedFiles()
179 | {
180 | Fetch();
181 |
182 | IList changedFiles = new List();
183 |
184 | using (var repo = new Repository(_providerOptions.LocalPath))
185 | {
186 | _logger.LogInformation("Checking for remote changes on {RemoteName}", repo.Head.TrackedBranch.RemoteName);
187 |
188 | foreach (TreeEntryChanges entry in repo.Diff.Compare(repo.Head.Tip.Tree, repo.Head.TrackedBranch.Tip.Tree))
189 | {
190 | if (entry.Exists)
191 | {
192 | _logger.LogInformation("File {Path} changed", entry.Path);
193 | changedFiles.Add(entry.Path.NormalizePathSeparators());
194 | }
195 | else
196 | {
197 | _logger.LogInformation("File {Path} no longer exists", entry.Path);
198 | }
199 | }
200 | }
201 |
202 | if (changedFiles.Count == 0)
203 | {
204 | _logger.LogInformation("No tree entry changes were detected");
205 |
206 | return changedFiles;
207 | }
208 |
209 | UpdateLocal();
210 |
211 | var filteredFiles = await ListPaths();
212 | changedFiles = filteredFiles.Intersect(changedFiles).ToList();
213 |
214 | _logger.LogInformation("{Count} files changed", changedFiles.Count);
215 |
216 | return changedFiles;
217 | }
218 |
219 | private void UpdateLocal()
220 | {
221 | using var repo = new Repository(_providerOptions.LocalPath);
222 | var options = new PullOptions
223 | {
224 | FetchOptions = new FetchOptions
225 | {
226 | CredentialsProvider = _credentialsHandler
227 | }
228 | };
229 |
230 | var signature = new Signature(new Identity("Configuration Service", "Configuration Service"), DateTimeOffset.Now);
231 |
232 | _logger.LogInformation("Pulling changes to local repository");
233 |
234 | var currentHash = repo.Head.Tip.Sha.Substring(0, 6);
235 |
236 | _logger.LogInformation("Current HEAD is [{CurrentHash}] '{MessageShort}'", currentHash, repo.Head.Tip.MessageShort);
237 |
238 | var result = Commands.Pull(repo, signature, options);
239 |
240 | _logger.LogInformation("Merge completed with status {Status}", result.Status);
241 |
242 | var newHash = result.Commit.Sha.Substring(0, 6);
243 |
244 | _logger.LogInformation("New HEAD is [{NewHash}] '{MessageShort}'", newHash, result.Commit.MessageShort);
245 | }
246 |
247 | private static void DeleteDirectory(string path)
248 | {
249 | foreach (var directory in Directory.EnumerateDirectories(path))
250 | {
251 | DeleteDirectory(directory);
252 | }
253 |
254 | foreach (var fileName in Directory.EnumerateFiles(path))
255 | {
256 | var fileInfo = new FileInfo(fileName)
257 | {
258 | Attributes = FileAttributes.Normal
259 | };
260 |
261 | fileInfo.Delete();
262 | }
263 |
264 | Directory.Delete(path);
265 | }
266 |
267 | private void Fetch()
268 | {
269 | using var repo = new Repository(_providerOptions.LocalPath);
270 | FetchOptions options = new FetchOptions
271 | {
272 | CredentialsProvider = _credentialsHandler
273 | };
274 |
275 | foreach (var remote in repo.Network.Remotes)
276 | {
277 | var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification);
278 |
279 | _logger.LogInformation("Fetching from remote {Name} at {Url}", remote.Name, remote.Url);
280 |
281 | Commands.Fetch(repo, remote.Name, refSpecs, options, string.Empty);
282 | }
283 | }
284 |
285 | private string GetRelativePath(string fullPath)
286 | {
287 | return Path.GetRelativePath(_providerOptions.LocalPath, fullPath).NormalizePathSeparators();
288 | }
289 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/Git/GitProviderOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ConfigurationService.Hosting.Providers.Git;
4 |
5 | ///
6 | /// Options for .
7 | ///
8 | public class GitProviderOptions
9 | {
10 | ///
11 | /// URI for the remote repository.
12 | ///
13 | public string RepositoryUrl { get; set; }
14 |
15 | ///
16 | /// Username for authentication.
17 | ///
18 | public string Username { get; set; }
19 |
20 | ///
21 | /// Password for authentication.
22 | ///
23 | public string Password { get; set; }
24 |
25 | ///
26 | /// The name of the branch to checkout. When unspecified the remote's default branch will be used instead.
27 | ///
28 | public string Branch { get; set; }
29 |
30 | ///
31 | /// Local path to clone into.
32 | ///
33 | public string LocalPath { get; set; }
34 |
35 | ///
36 | /// The search string to use as a filter against the names of files. Defaults to all files ('*').
37 | ///
38 | public string SearchPattern { get; set; }
39 |
40 | ///
41 | /// The interval to check for for remote changes. Defaults to 60 seconds.
42 | ///
43 | public TimeSpan PollingInterval { get; set; } = TimeSpan.FromSeconds(60);
44 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/IProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace ConfigurationService.Hosting.Providers;
7 |
8 | public interface IProvider
9 | {
10 | string Name { get; }
11 |
12 | Task Watch(Func, Task> onChange, CancellationToken cancellationToken = default);
13 |
14 | void Initialize();
15 |
16 | Task GetConfiguration(string name);
17 |
18 | Task GetHash(string name);
19 |
20 | Task> ListPaths();
21 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/Vault/VaultProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text.Json;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.Extensions.Logging;
9 | using VaultSharp;
10 |
11 | namespace ConfigurationService.Hosting.Providers.Vault;
12 |
13 | public class VaultProvider : IProvider
14 | {
15 | private readonly ILogger _logger;
16 |
17 | private readonly VaultProviderOptions _providerOptions;
18 |
19 | private IVaultClient _vaultClient;
20 | private readonly IDictionary _secretVersions = new Dictionary();
21 |
22 | public string Name => "Vault";
23 |
24 | public VaultProvider(ILogger logger, VaultProviderOptions providerOptions)
25 | {
26 | _logger = logger;
27 | _providerOptions = providerOptions;
28 |
29 | if (string.IsNullOrWhiteSpace(_providerOptions.ServerUri))
30 | {
31 | throw new ProviderOptionNullException(nameof(_providerOptions.ServerUri));
32 | }
33 |
34 | if (string.IsNullOrWhiteSpace(_providerOptions.Path))
35 | {
36 | throw new ProviderOptionNullException(nameof(_providerOptions.Path));
37 | }
38 |
39 | if (_providerOptions.AuthMethodInfo == null)
40 | {
41 | throw new ProviderOptionNullException(nameof(_providerOptions.AuthMethodInfo));
42 | }
43 | }
44 |
45 | public async Task Watch(Func, Task> onChange, CancellationToken cancellationToken = default)
46 | {
47 | while (!cancellationToken.IsCancellationRequested)
48 | {
49 | try
50 | {
51 | var changes = new List();
52 |
53 | var paths = await ListPaths();
54 |
55 | foreach (var path in paths)
56 | {
57 | var metadata = await _vaultClient.V1.Secrets.KeyValue.V2.ReadSecretMetadataAsync(path, _providerOptions.Path);
58 |
59 | _secretVersions.TryGetValue(path, out int version);
60 |
61 | if (version != metadata.Data.CurrentVersion)
62 | {
63 | changes.Add(path);
64 |
65 | _secretVersions[path] = metadata.Data.CurrentVersion;
66 | }
67 | }
68 |
69 | if (changes.Count > 0)
70 | {
71 | await onChange(changes);
72 | }
73 | }
74 | catch (Exception ex)
75 | {
76 | _logger.LogError(ex, "An unhandled exception occurred while attempting to poll for changes");
77 | }
78 |
79 | var delayDate = DateTime.UtcNow.Add(_providerOptions.PollingInterval);
80 |
81 | _logger.LogInformation("Next polling period will begin in {PollingInterval:c} at {DelayDate}",
82 | _providerOptions.PollingInterval, delayDate);
83 |
84 | await Task.Delay(_providerOptions.PollingInterval, cancellationToken);
85 | }
86 | }
87 |
88 | public void Initialize()
89 | {
90 | _logger.LogInformation("Initializing {Name} provider with options {@Options}", Name, new
91 | {
92 | _providerOptions.ServerUri,
93 | _providerOptions.Path
94 | });
95 |
96 | var vaultClientSettings = new VaultClientSettings(_providerOptions.ServerUri, _providerOptions.AuthMethodInfo);
97 |
98 | _vaultClient = new VaultClient(vaultClientSettings);
99 | }
100 |
101 | public async Task GetConfiguration(string name)
102 | {
103 | var secret = await _vaultClient.V1.Secrets.KeyValue.V2.ReadSecretAsync(name, null, _providerOptions.Path);
104 |
105 | if (secret == null)
106 | {
107 | _logger.LogInformation("Secret does not exist at {Name}", name);
108 | return null;
109 | }
110 |
111 | await using var stream = new MemoryStream();
112 | await JsonSerializer.SerializeAsync(stream, secret.Data.Data);
113 | return stream.ToArray();
114 | }
115 |
116 | public async Task GetHash(string name)
117 | {
118 | var bytes = await GetConfiguration(name);
119 |
120 | return Hasher.CreateHash(bytes);
121 | }
122 |
123 | public async Task> ListPaths()
124 | {
125 | _logger.LogInformation("Listing paths at {Path}", _providerOptions.Path);
126 |
127 | var secret = await _vaultClient.V1.Secrets.KeyValue.V2.ReadSecretPathsAsync(null, _providerOptions.Path);
128 | var paths = secret.Data.Keys.ToList();
129 |
130 | _logger.LogInformation("{Count} paths found", paths.Count);
131 |
132 | return paths;
133 | }
134 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Providers/Vault/VaultProviderOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using VaultSharp.V1.AuthMethods;
3 |
4 | namespace ConfigurationService.Hosting.Providers.Vault;
5 |
6 | ///
7 | /// Options for .
8 | ///
9 | public class VaultProviderOptions
10 | {
11 | ///
12 | /// The Vault Server Uri with port.
13 | ///
14 | public string ServerUri { get; set; }
15 |
16 | ///
17 | /// The path where the kv secrets engine is enabled.
18 | ///
19 | public string Path { get; set; }
20 |
21 | ///
22 | /// The auth method to be used to acquire a vault token.
23 | ///
24 | public IAuthMethodInfo AuthMethodInfo { get; set; }
25 |
26 | ///
27 | /// The interval to check for for remote changes. Defaults to 60 seconds.
28 | ///
29 | public TimeSpan PollingInterval { get; set; } = TimeSpan.FromSeconds(60);
30 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Publishers/IPublisher.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace ConfigurationService.Hosting.Publishers;
4 |
5 | public interface IPublisher
6 | {
7 | void Initialize();
8 |
9 | Task Publish(string topic, string message);
10 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Publishers/Nats/NatsPublisher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Logging;
5 | using NATS.Client;
6 |
7 | namespace ConfigurationService.Hosting.Publishers.Nats;
8 |
9 | public class NatsPublisher : IPublisher
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly Options _options;
14 | private static IConnection _connection;
15 |
16 | public NatsPublisher(ILogger logger, Options options)
17 | {
18 | _logger = logger;
19 |
20 | _options = options ?? throw new ArgumentNullException(nameof(options));
21 | }
22 |
23 | public void Initialize()
24 | {
25 | _options.AsyncErrorEventHandler += (sender, args) => { _logger.LogError("NATS replied with an error message: {Message}", args.Error); };
26 |
27 | _options.ClosedEventHandler += (sender, args) => { _logger.LogError(args.Error, "NATS connection was closed"); };
28 |
29 | _options.DisconnectedEventHandler += (sender, args) => { _logger.LogError(args.Error, "NATS connection was disconnected"); };
30 |
31 | _options.ReconnectedEventHandler += (sender, args) => { _logger.LogInformation("NATS connection was restored"); };
32 |
33 | var connectionFactory = new ConnectionFactory();
34 | _connection = connectionFactory.CreateConnection(_options);
35 |
36 | _logger.LogInformation("NATS publisher initialized");
37 | }
38 |
39 | public Task Publish(string topic, string message)
40 | {
41 | _logger.LogInformation("Publishing message to NATS with subject {Subject}", topic);
42 |
43 | var data = Encoding.UTF8.GetBytes(message);
44 |
45 | _connection.Publish(topic, data);
46 |
47 | return Task.CompletedTask;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Publishers/RabbitMq/RabbitMqOptions.cs:
--------------------------------------------------------------------------------
1 | namespace ConfigurationService.Hosting.Publishers.RabbitMq;
2 |
3 | public class RabbitMqOptions
4 | {
5 | /// The host to connect to.
6 | /// Defaults to "localhost".
7 | public string HostName { get; set; } = "localhost";
8 |
9 | ///
10 | /// Virtual host to access during this connection.
11 | /// Defaults to "/".
12 | ///
13 | public string VirtualHost { get; set; } = "/";
14 |
15 | ///
16 | /// Username to use when authenticating to the server.
17 | /// Defaults to "guest".
18 | ///
19 | public string UserName { get; set; } = "guest";
20 |
21 | ///
22 | /// Password to use when authenticating to the server.
23 | /// Defaults to "guest".
24 | ///
25 | public string Password { get; set; } = "guest";
26 |
27 | ///
28 | /// Name of the fanout exchange.
29 | /// Defaults to "configuration-service"
30 | ///
31 | public string ExchangeName { get; set; } = "configuration-service";
32 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Publishers/RabbitMq/RabbitMqPublisher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Logging;
5 | using RabbitMQ.Client;
6 |
7 | namespace ConfigurationService.Hosting.Publishers.RabbitMq;
8 |
9 | public class RabbitMqPublisher : IPublisher
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly RabbitMqOptions _options;
14 | private string _exchangeName;
15 | private static IModel _channel;
16 |
17 | public RabbitMqPublisher(ILogger logger, RabbitMqOptions options)
18 | {
19 | _logger = logger;
20 |
21 | _options = options ?? throw new ArgumentNullException(nameof(options));
22 | }
23 |
24 | public void Initialize()
25 | {
26 | var factory = new ConnectionFactory
27 | {
28 | HostName = _options.HostName,
29 | VirtualHost = _options.VirtualHost,
30 | UserName = _options.UserName,
31 | Password = _options.Password
32 | };
33 |
34 | _exchangeName = _options.ExchangeName;
35 |
36 | var connection = factory.CreateConnection();
37 | _channel = connection.CreateModel();
38 |
39 | connection.CallbackException += (sender, args) => { _logger.LogError(args.Exception, "RabbitMQ callback exception"); };
40 |
41 | connection.ConnectionBlocked += (sender, args) => { _logger.LogError("RabbitMQ connection is blocked. Reason: {Reason}", args.Reason); };
42 |
43 | connection.ConnectionShutdown += (sender, args) => { _logger.LogError("RabbitMQ connection was shut down. Reason: {ReplyText}", args.ReplyText); };
44 |
45 | connection.ConnectionUnblocked += (sender, args) => { _logger.LogInformation("RabbitMQ connection was unblocked"); };
46 |
47 | _channel.ExchangeDeclare(_exchangeName, ExchangeType.Fanout);
48 |
49 | _logger.LogInformation("RabbitMQ publisher initialized");
50 | }
51 |
52 | public Task Publish(string topic, string message)
53 | {
54 | _logger.LogInformation("Publishing message with routing key {RoutingKey}", topic);
55 |
56 | var body = Encoding.UTF8.GetBytes(message);
57 | _channel.BasicPublish(_exchangeName, topic, null, body);
58 |
59 | return Task.CompletedTask;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Hosting/Publishers/Redis/RedisPublisher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Logging;
5 | using StackExchange.Redis;
6 |
7 | namespace ConfigurationService.Hosting.Publishers.Redis;
8 |
9 | public class RedisPublisher : IPublisher
10 | {
11 | private readonly ILogger _logger;
12 |
13 | private readonly ConfigurationOptions _options;
14 | private static IConnectionMultiplexer _connection;
15 |
16 | public RedisPublisher(ILogger logger, ConfigurationOptions configuration)
17 | {
18 | _logger = logger;
19 |
20 | _options = configuration ?? throw new ArgumentNullException(nameof(configuration));
21 | }
22 |
23 | public void Initialize()
24 | {
25 |
26 | using (var writer = new StringWriter())
27 | {
28 | _connection = ConnectionMultiplexer.Connect(_options, writer);
29 |
30 | _logger.LogTrace("Redis publisher connected with log:\r\n{Log}", writer);
31 | }
32 |
33 | _connection.ErrorMessage += (sender, args) => { _logger.LogError("Redis replied with an error message: {Message}", args.Message); };
34 |
35 | _connection.ConnectionFailed += (sender, args) => { _logger.LogError(args.Exception, "Redis connection failed"); };
36 |
37 | _connection.ConnectionRestored += (sender, args) => { _logger.LogInformation("Redis connection restored"); };
38 |
39 | _logger.LogInformation("Redis publisher initialized");
40 | }
41 |
42 | public async Task Publish(string topic, string message)
43 | {
44 | _logger.LogInformation("Publishing message to channel {Channel}", topic);
45 |
46 | var publisher = _connection.GetSubscriber();
47 |
48 | var clientCount = await publisher.PublishAsync(topic, message);
49 |
50 | _logger.LogInformation("Message to channel {Channel} was received by {ClientCount} clients", topic, clientCount);
51 | }
52 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/ConfigurationService.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | PreserveNewest
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/Files/test.ini:
--------------------------------------------------------------------------------
1 | [config]
2 | value = test
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/Files/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "config":
3 | {
4 | "value": "test"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/Files/test.xml:
--------------------------------------------------------------------------------
1 |
2 | test
3 |
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/Files/test.yaml:
--------------------------------------------------------------------------------
1 | config:
2 | value: test
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/ParserTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using ConfigurationService.Client.Parsers;
4 | using Xunit;
5 |
6 | namespace ConfigurationService.Test;
7 |
8 | public class ParserTests
9 | {
10 | [Theory]
11 | [InlineData("Files/test.json")]
12 | public async Task Parses_Valid_Json(string path)
13 | {
14 | await using var stream = new FileStream(path, FileMode.Open);
15 | var parser = new JsonConfigurationFileParser();
16 | var output = parser.Parse(stream);
17 |
18 | Assert.NotEmpty(output);
19 | }
20 |
21 | [Theory]
22 | [InlineData("Files/test.xml")]
23 | public async Task Parses_Valid_Xml(string path)
24 | {
25 | await using var stream = new FileStream(path, FileMode.Open);
26 | var parser = new XmlConfigurationFileParser();
27 | var output = parser.Parse(stream);
28 |
29 | Assert.NotEmpty(output);
30 | }
31 |
32 | [Theory]
33 | [InlineData("Files/test.yaml")]
34 | public async Task Parses_Valid_Yaml(string path)
35 | {
36 | await using var stream = new FileStream(path, FileMode.Open);
37 | var parser = new YamlConfigurationFileParser();
38 | var output = parser.Parse(stream);
39 |
40 | Assert.NotEmpty(output);
41 | }
42 |
43 | [Theory]
44 | [InlineData("Files/test.ini")]
45 | public async Task Parses_Valid_Ini(string path)
46 | {
47 | await using var stream = new FileStream(path, FileMode.Open);
48 | var parser = new IniConfigurationFileParser();
49 | var output = parser.Parse(stream);
50 |
51 | Assert.NotEmpty(output);
52 | }
53 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.Test/PublishTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading.Tasks;
5 | using ConfigurationService.Hosting;
6 | using ConfigurationService.Hosting.Providers;
7 | using ConfigurationService.Hosting.Publishers;
8 | using Microsoft.Extensions.Logging;
9 | using NSubstitute;
10 | using Xunit;
11 |
12 | namespace ConfigurationService.Test;
13 |
14 | public class PublishTests
15 | {
16 | private readonly ILogger _logger;
17 | private readonly IProvider _provider;
18 | private readonly IPublisher _publisher;
19 | private readonly IConfigurationService _configurationService;
20 |
21 | public PublishTests()
22 | {
23 | _logger = Substitute.For>();
24 | _publisher = Substitute.For();
25 | _provider = SetupStorageProvider();
26 | _configurationService = new Hosting.ConfigurationService(_logger, _provider, _publisher);
27 | }
28 |
29 | [Fact]
30 | public async Task Publish_Invoked_on_Initialization()
31 | {
32 | await _configurationService.Initialize();
33 |
34 | await _publisher.Received().Publish(Arg.Any(), Arg.Any());
35 | }
36 |
37 | [Fact]
38 | public async Task Publish_Invoked_on_Change()
39 | {
40 | await _configurationService.OnChange(ListRandomFiles(1));
41 |
42 | await _publisher.Received(1).Publish(Arg.Any(), Arg.Any());
43 | }
44 |
45 | [Fact]
46 | public async Task Publish_Invoked_on_PublishChanges()
47 | {
48 | await _configurationService.PublishChanges(ListRandomFiles(1));
49 |
50 | await _publisher.Received(1).Publish(Arg.Any(), Arg.Any());
51 | }
52 |
53 | [Fact]
54 | public async Task Publish_Invoked_on_Change_for_Each_File()
55 | {
56 | var fileCount = 5;
57 | await _configurationService.OnChange(ListRandomFiles(fileCount));
58 |
59 | await _publisher.Received(fileCount).Publish(Arg.Any(), Arg.Any());
60 | }
61 |
62 | [Fact]
63 | public async Task Publish_Is_Not_Invoked_when_No_Change()
64 | {
65 | await _configurationService.OnChange(new List());
66 |
67 | await _publisher.DidNotReceive().Publish(Arg.Any(), Arg.Any());
68 | }
69 |
70 | [Fact]
71 | public async Task Publish_Does_Not_Fail_when_No_Publisher_Registered()
72 | {
73 | var configurationService = new Hosting.ConfigurationService(_logger, _provider);
74 | await configurationService.PublishChanges(ListRandomFiles(1));
75 |
76 | await _publisher.DidNotReceive().Publish(Arg.Any(), Arg.Any());
77 | }
78 |
79 | [Fact]
80 | public async Task Publish_Is_Not_Invoked_when_No_Publisher_Registered()
81 | {
82 | var configurationService = new Hosting.ConfigurationService(_logger, _provider);
83 | await configurationService.OnChange(ListRandomFiles(1));
84 |
85 | await _publisher.DidNotReceive().Publish(Arg.Any(), Arg.Any());
86 | }
87 |
88 | private IProvider SetupStorageProvider()
89 | {
90 | var storageProvider = Substitute.For();
91 | storageProvider.ListPaths().Returns(ListRandomFiles(1));
92 | storageProvider.GetConfiguration(Arg.Any()).Returns(name => Encoding.UTF8.GetBytes($"{{ \"name\": \"{name}\" }}"));
93 | return storageProvider;
94 | }
95 |
96 | private static IEnumerable ListRandomFiles(int count)
97 | {
98 | var list = new List();
99 |
100 | for (int i = 0; i < count; i++)
101 | {
102 | list.Add($"{Guid.NewGuid()}.json");
103 | }
104 |
105 | return list;
106 | }
107 | }
--------------------------------------------------------------------------------
/src/ConfigurationService.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30225.117
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationService.Hosting", "ConfigurationService.Hosting\ConfigurationService.Hosting.csproj", "{791DC3A5-9493-41AE-A605-E45142C5246D}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationService.Client", "ConfigurationService.Client\ConfigurationService.Client.csproj", "{7A654435-A733-4BCD-8477-77065F75C438}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationService.Test", "ConfigurationService.Test\ConfigurationService.Test.csproj", "{D76FFB0A-603C-47B0-B5F9-B85092E091D7}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationService.Samples.Client", "samples\ConfigurationService.Samples.Client\ConfigurationService.Samples.Client.csproj", "{6E272D96-7293-4FB8-BAEC-4481223385D9}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationService.Samples.Host", "samples\ConfigurationService.Samples.Host\ConfigurationService.Samples.Host.csproj", "{042D1AF2-C723-4D39-AE9D-73E75D24F93F}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {791DC3A5-9493-41AE-A605-E45142C5246D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {791DC3A5-9493-41AE-A605-E45142C5246D}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {791DC3A5-9493-41AE-A605-E45142C5246D}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {791DC3A5-9493-41AE-A605-E45142C5246D}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {7A654435-A733-4BCD-8477-77065F75C438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {7A654435-A733-4BCD-8477-77065F75C438}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {7A654435-A733-4BCD-8477-77065F75C438}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {7A654435-A733-4BCD-8477-77065F75C438}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {D76FFB0A-603C-47B0-B5F9-B85092E091D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {D76FFB0A-603C-47B0-B5F9-B85092E091D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {D76FFB0A-603C-47B0-B5F9-B85092E091D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {D76FFB0A-603C-47B0-B5F9-B85092E091D7}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {6E272D96-7293-4FB8-BAEC-4481223385D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {6E272D96-7293-4FB8-BAEC-4481223385D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {6E272D96-7293-4FB8-BAEC-4481223385D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {6E272D96-7293-4FB8-BAEC-4481223385D9}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {042D1AF2-C723-4D39-AE9D-73E75D24F93F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {042D1AF2-C723-4D39-AE9D-73E75D24F93F}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {042D1AF2-C723-4D39-AE9D-73E75D24F93F}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {042D1AF2-C723-4D39-AE9D-73E75D24F93F}.Release|Any CPU.Build.0 = Release|Any CPU
42 | EndGlobalSection
43 | GlobalSection(SolutionProperties) = preSolution
44 | HideSolutionNode = FALSE
45 | EndGlobalSection
46 | GlobalSection(ExtensibilityGlobals) = postSolution
47 | SolutionGuid = {FC20561E-ACA1-4E8E-819D-AAA8A39A2721}
48 | EndGlobalSection
49 | EndGlobal
50 |
--------------------------------------------------------------------------------
/src/ConfigurationService.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Client/ConfigWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Options;
5 |
6 | namespace ConfigurationService.Samples.Client;
7 |
8 | public class ConfigWriter
9 | {
10 | private readonly IOptionsMonitor _testConfig;
11 |
12 | public ConfigWriter(IOptionsMonitor testConfig)
13 | {
14 | _testConfig = testConfig;
15 | }
16 |
17 | public async Task Write(CancellationToken cancellationToken = default)
18 | {
19 | while (!cancellationToken.IsCancellationRequested)
20 | {
21 | var config = _testConfig.CurrentValue;
22 | Console.WriteLine(config.Text);
23 |
24 | await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Client/ConfigurationService.Samples.Client.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | PreserveNewest
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Client/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Logging;
5 | using ConfigurationService.Client;
6 | using ConfigurationService.Client.Parsers;
7 |
8 | namespace ConfigurationService.Samples.Client;
9 |
10 | static class Program
11 | {
12 | static async Task Main()
13 | {
14 | var loggerFactory = LoggerFactory.Create(builder =>
15 | {
16 | builder.AddConsole();
17 | });
18 |
19 | IConfiguration localConfiguration = new ConfigurationBuilder()
20 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
21 | .Build();
22 |
23 | var configuration = new ConfigurationBuilder()
24 | .AddConfiguration(localConfiguration)
25 | .AddRemoteConfiguration(o =>
26 | {
27 | o.ServiceUri = "http://localhost:5000/configuration/";
28 | o.AddConfiguration(c =>
29 | {
30 | c.ConfigurationName = "test.json";
31 | c.ReloadOnChange = true;
32 | c.Optional = false;
33 | });
34 | o.AddConfiguration(c =>
35 | {
36 | c.ConfigurationName = "test.yaml";
37 | c.ReloadOnChange = true;
38 | c.Optional = false;
39 | c.Parser = new YamlConfigurationFileParser();
40 | });
41 | o.AddRedisSubscriber("localhost:6379");
42 | o.AddLoggerFactory(loggerFactory);
43 | })
44 | .Build();
45 |
46 | var services = new ServiceCollection();
47 | services.AddSingleton();
48 | services.Configure(configuration.GetSection("Config"));
49 |
50 | var serviceProvider = services.BuildServiceProvider();
51 |
52 | var configWriter = serviceProvider.GetService();
53 |
54 | await configWriter.Write();
55 | }
56 | }
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Client/TestConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ConfigurationService.Samples.Client;
2 |
3 | public class TestConfig
4 | {
5 | public string Text { get; set; }
6 | }
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Client/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Host/ConfigurationService.Samples.Host.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Host/Program.cs:
--------------------------------------------------------------------------------
1 | using ConfigurationService.Hosting;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 |
5 | builder.Services.AddConfigurationService()
6 | .AddGitProvider(c =>
7 | {
8 | c.RepositoryUrl = "https://github.com/jamespratt/configuration-test.git";
9 | c.LocalPath = "C:/local-repo";
10 | })
11 | .AddRedisPublisher("localhost:6379");
12 |
13 | var app = builder.Build();
14 |
15 | app.UseRouting();
16 |
17 | app.UseEndpoints(endpoints =>
18 | {
19 | endpoints.MapConfigurationService();
20 | });
21 |
22 | app.Run();
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Host/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "ConfigurationService.Samples.Host": {
5 | "commandName": "Project",
6 | "launchBrowser": true,
7 | "launchUrl": "configuration",
8 | "applicationUrl": "http://localhost:5000",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Host/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/samples/ConfigurationService.Samples.Host/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------