├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CorrelationId.sln ├── LICENSE.txt ├── README.md ├── azure-pipelines.yml ├── docs ├── index.md └── releasenotes.md ├── samples └── 3.1 │ ├── MvcSample │ ├── Controllers │ │ └── WeatherForecastController.cs │ ├── DoNothingCorrelationIdProvider.cs │ ├── MvcSample.csproj │ ├── NoOpDelegatingHandler.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ServiceWhichUsesCorrelationContext.cs │ ├── Startup.cs │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── MvcSampleTests │ ├── MvcSampleTests.csproj │ └── ServiceWhichUsesCorrelationContextTests.cs ├── src └── CorrelationId │ ├── Abstractions │ ├── ICorrelationContextAccessor.cs │ ├── ICorrelationContextFactory.cs │ └── ICorrelationIdProvider.cs │ ├── CorrelationContext.cs │ ├── CorrelationContextAccessor.cs │ ├── CorrelationContextFactory.cs │ ├── CorrelationId.csproj │ ├── CorrelationIdExtensions.cs │ ├── CorrelationIdMiddleware.cs │ ├── CorrelationIdOptions.cs │ ├── DependencyInjection │ ├── CorrelationIdBuilder.cs │ ├── CorrelationIdBuilderExtensions.cs │ ├── CorrelationIdServiceCollectionExtensions.cs │ └── ICorrelationIdBuilder.cs │ ├── HttpClient │ ├── CorrelationIdHandler.cs │ └── HttpClientBuilderExtensions.cs │ └── Providers │ ├── GuidCorrelationIdProvider.cs │ └── TraceIdCorrelationIdProvider.cs └── test └── CorrelationId.Tests ├── CorrelationId.Tests.csproj └── CorrelationIdMiddlewareTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [stevejgordon] 2 | custom: ["https://www.buymeacoffee.com/stevejgordon"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | os: 3 | - linux 4 | 5 | sudo: required 6 | mono: none 7 | dist: xenial 8 | dotnet: 3.1.100 9 | 10 | install: 11 | - dotnet restore 12 | 13 | before_script: 14 | - sudo apt-get install dotnet-sdk-2.1 15 | 16 | script: 17 | - dotnet build 18 | - dotnet test test/CorrelationId.Tests/CorrelationId.Tests.csproj 19 | -------------------------------------------------------------------------------- /CorrelationId.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29613.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorrelationId", "src\CorrelationId\CorrelationId.csproj", "{865886FC-EBBE-49E1-8F49-FECD0408EF6E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9A14C73A-D8FC-40C3-81FC-D0687F43FEAF}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{EAF27B74-0B27-4BEE-9F82-DD5812B21B17}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{34C0F65A-8BF2-40DA-B0E7-844930EE2A7B}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CorrelationId.Tests", "test\CorrelationId.Tests\CorrelationId.Tests.csproj", "{41035337-9829-4A6A-8EA9-42CC94734CE6}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{1DC2BB08-1FE8-43B1-BD2E-F55DA4B54038}" 17 | ProjectSection(SolutionItems) = preProject 18 | docs\index.md = docs\index.md 19 | README.md = README.md 20 | docs\releasenotes.md = docs\releasenotes.md 21 | EndProjectSection 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{5961B376-2D2A-441B-BF59-9A797AC9178B}" 24 | ProjectSection(SolutionItems) = preProject 25 | .travis.yml = .travis.yml 26 | azure-pipelines.yml = azure-pipelines.yml 27 | EndProjectSection 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3.1", "3.1", "{E28C5481-A68F-44AF-983F-EA127E70621A}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcSample", "samples\3.1\MvcSample\MvcSample.csproj", "{9393676B-BE08-44C9-B556-7BE31CFA5B86}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcSampleTests", "samples\3.1\MvcSampleTests\MvcSampleTests.csproj", "{0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {865886FC-EBBE-49E1-8F49-FECD0408EF6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {865886FC-EBBE-49E1-8F49-FECD0408EF6E}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {865886FC-EBBE-49E1-8F49-FECD0408EF6E}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {865886FC-EBBE-49E1-8F49-FECD0408EF6E}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {41035337-9829-4A6A-8EA9-42CC94734CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {41035337-9829-4A6A-8EA9-42CC94734CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {41035337-9829-4A6A-8EA9-42CC94734CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {41035337-9829-4A6A-8EA9-42CC94734CE6}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {9393676B-BE08-44C9-B556-7BE31CFA5B86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {9393676B-BE08-44C9-B556-7BE31CFA5B86}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {9393676B-BE08-44C9-B556-7BE31CFA5B86}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {9393676B-BE08-44C9-B556-7BE31CFA5B86}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F}.Release|Any CPU.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(NestedProjects) = preSolution 62 | {865886FC-EBBE-49E1-8F49-FECD0408EF6E} = {9A14C73A-D8FC-40C3-81FC-D0687F43FEAF} 63 | {41035337-9829-4A6A-8EA9-42CC94734CE6} = {EAF27B74-0B27-4BEE-9F82-DD5812B21B17} 64 | {E28C5481-A68F-44AF-983F-EA127E70621A} = {34C0F65A-8BF2-40DA-B0E7-844930EE2A7B} 65 | {9393676B-BE08-44C9-B556-7BE31CFA5B86} = {E28C5481-A68F-44AF-983F-EA127E70621A} 66 | {0E2E2678-4DBB-4560-AF0E-86ECAF8D9B6F} = {E28C5481-A68F-44AF-983F-EA127E70621A} 67 | EndGlobalSection 68 | GlobalSection(ExtensibilityGlobals) = postSolution 69 | SolutionGuid = {C0A37404-C50B-472E-9491-DDE2A4BDA882} 70 | EndGlobalSection 71 | EndGlobal 72 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Steve Gordon 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Correlation ID 2 | 3 | Correlations IDs are used in distributed applications to trace requests across multiple services. This library and package provides a lightweight correlation ID approach. When enabled, request headers are checked for a correlation ID from the consumer. If found, this correlation ID is attached to the Correlation Context which can be used to access the current correlation ID where it is required for logging etc. 4 | 5 | Optionally, this correlation ID can be attached to downstream HTTP calls made via a `HttpClient` instance created by the `IHttpClientFactory`. 6 | 7 | **NOTE: While I plan to add a few more features to this library, I believe it is close to feature complete for the scenario it was designed for. I recommend looking at [built-in tracing for .NET apps](https://devblogs.microsoft.com/aspnet/observability-asp-net-core-apps/#adding-tracing-to-a-net-core-application) for more complete and automated application tracing.** 8 | 9 | ## Release Notes 10 | 11 | [Change history and release notes](https://stevejgordon.github.io/CorrelationId/releasenotes). 12 | 13 | ## Supported Runtimes 14 | - .NET Standard 2.0+ 15 | 16 | | Package | NuGet Stable | NuGet Pre-release | Downloads | Travis CI | Azure Pipelines | 17 | | ------- | ------------ | ----------------- | --------- | --------- | ----------------| 18 | | [CorrelationId](https://www.nuget.org/packages/CorrelationId/) | [![NuGet](https://img.shields.io/nuget/v/CorrelationId.svg)](https://www.nuget.org/packages/CorrelationId) | [![NuGet](https://img.shields.io/nuget/vpre/CorrelationId.svg)](https://www.nuget.org/packages/CorrelationId) | [![Nuget](https://img.shields.io/nuget/dt/CorrelationId.svg)](https://www.nuget.org/packages/CorrelationId) | [![Build Status](https://travis-ci.org/stevejgordon/CorrelationId.svg?branch=master)](https://travis-ci.org/stevejgordon/CorrelationId) | [![Build Status](https://dev.azure.com/stevejgordon/CorrelationId/_apis/build/status/stevejgordon.CorrelationId?branchName=master)](https://dev.azure.com/stevejgordon/CorrelationId/_build/latest?definitionId=1&branchName=master) | 19 | 20 | ## Installation 21 | 22 | You should install [CorrelationId from NuGet](https://www.nuget.org/packages/CorrelationId/): 23 | 24 | ```ps 25 | Install-Package CorrelationId 26 | ``` 27 | 28 | This command from Package Manager Console will download and install CorrelationId and all required dependencies. 29 | 30 | All stable and some pre-release packages are available on NuGet. 31 | 32 | ## Quick Start 33 | 34 | ### Register with DI 35 | 36 | Inside `ConfigureServices` add the required correlation ID services, with common defaults. 37 | 38 | ```csharp 39 | services.AddDefaultCorrelationId 40 | ``` 41 | 42 | This registers a correlation ID provider which generates new IDs based on a random GUID. 43 | 44 | ### Add the middleware 45 | 46 | Register the middleware into the pipeline. This should occur before any downstream middleware which requires the correlation ID. Normally this will be registered very early in the middleware pipeline. 47 | 48 | ```csharp 49 | app.UseCorrelationId(); 50 | ``` 51 | 52 | Where you need to access the correlation ID, you may request the `ICorrelationContextAccessor` from DI. 53 | 54 | ```csharp 55 | public class TransientClass 56 | { 57 | private readonly ICorrelationContextAccessor _correlationContext; 58 | 59 | public TransientClass(ICorrelationContextAccessor correlationContext) 60 | { 61 | _correlationContext = correlationContext; 62 | } 63 | 64 | ... 65 | } 66 | ``` 67 | 68 | See the [sample app](https://github.com/stevejgordon/CorrelationId/tree/master/samples/3.1/MvcSample) for example usage. 69 | 70 | Full documentation can be found in the [wiki](https://github.com/stevejgordon/CorrelationId/wiki). 71 | 72 | ## Known Issue with ASP.NET Core 2.2.0 73 | 74 | It appears that a [regression in the code for ASP.NET Core 2.2.0](https://github.com/aspnet/AspNetCore/issues/5144) means that setting the TraceIdentifier on the context via middleware results in the context becoming null when accessed further down in the pipeline. A fix is was released in 2.2.2. 75 | 76 | A workaround at this time is to disable the behaviour of updating the TraceIdentifier using the options when adding the middleware. 77 | 78 | ## Support 79 | 80 | If this library has helped you, feel free to [buy me a coffee](https://www.buymeacoffee.com/stevejgordon) or see the "Sponsor" link [at the top of the GitHub page](https://github.com/stevejgordon/CorrelationId). 81 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | 4 | pool: 5 | vmImage: 'ubuntu-latest' 6 | 7 | variables: 8 | buildConfiguration: 'Release' 9 | 10 | steps: 11 | - task: UseDotNet@2 12 | displayName: ".NET Core 3.1.x" 13 | inputs: 14 | version: '3.1.x' 15 | packageType: sdk 16 | 17 | - task: UseDotNet@2 18 | inputs: 19 | version: '3.1.x' 20 | - script: dotnet build --configuration $(buildConfiguration) 21 | displayName: 'dotnet build $(buildConfiguration)' 22 | 23 | - task: DotNetCoreCLI@2 24 | inputs: 25 | command: test 26 | projects: '/test/*/*.csproj' 27 | arguments: '--configuration $(buildConfiguration)' 28 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Correlation ID 2 | - 3 | 4 | # What is Correlation ID? 5 | 6 | The correlation ID library provides a lightweight solution to parse a configurable correlation ID header on incoming requests which can then be accessed within the application code and optionally included on outgoing requests. 7 | 8 | ## Release Notes 9 | 10 | [Change history release notes](https://stevejgordon.github.io/CorrelationId/releasenotes) -------------------------------------------------------------------------------- /docs/releasenotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | Packages are available on NuGet: [CorrelationId](https://www.nuget.org/packages/CorrelationId/). 4 | 5 | ## v3.0.1 6 | 7 | ### Bug Fixes 8 | 9 | * Do not throw exception when Correlation accessor has Context as null - [#96](https://github.com/stevejgordon/CorrelationId/pull/96) 10 | 11 | ## v3.0.0 12 | 13 | Several requested features have been added which provide more control over correlation ID generation and customisation. 14 | 15 | This release includes several breaking changes, and upgrading will require some changes to consuming code. 16 | 17 | A major new feature in this release is the concept of an `ICorrelationIdProvider`. This interface defines an abstraction for generating correlation IDs. The library includes two provider implementations which include the previous behaviour. The `GuidCorrelationIdProvider`, when registered will generate new GUID-based correlations IDs. The `TraceIdCorrelationIdProvider` will generate the correlation ID, setting it to the same value as the TraceIdentifier string on the `HttpContext`. 18 | 19 | Only one provider may be registered. Registering multiple providers will cause an `InvalidOperationException` to be thrown. 20 | 21 | **BREAKING CHANGES** 22 | 23 | ### Registering services 24 | 25 | Changes have been made to the registration methods on the `IServiceCollection` to support the new providers concept. 26 | 27 | When registering the required correlation ID services by calling the `AddCorrelationId` method, this now returns an `ICorrelationIdBuilder` that supports additional methods that can be used to configure the provider, which will be used. This method does not set a default provider, so it is expected that one of the appropriate `ICorrelationIdBuilder` builder methods be called. 28 | 29 | Alternatively, the `AddCorrelationId` method can be called, which accepts the type to use for the `ICorrelationIdProvider`. 30 | 31 | Finally, the `AddDefaultCorrelationId` method may be used, which returns the `IServiceCollection` and which does not support further configuration of the correlation ID configuration using the builder. In this case, the default provider will be the `GuidCorrelationIdProvider`. This method exists for those wanting to chain `IServiceCollection` extension methods and where the default GUID provider is suitable. 32 | 33 | ### Configuration Options 34 | 35 | A change has been made to how the `CorrelationIdOptions` are configured for the correlation ID behaviour. Previously, a `CorrelationIdOptions` instance could be passed to the `UseCorrelationId` extension method on the `IApplicationBuilder`. This is no longer the correct way to register options. Instead, options can be configured via Action delegate overloads on the `IServiceCollection` extensions methods. 36 | 37 | ``` 38 | services.AddDefaultCorrelationId(options => 39 | { 40 | options.CorrelationIdGenerator = () => "Foo"; 41 | options.AddToLoggingScope = true; 42 | options.EnforceHeader = true; 43 | options.IgnoreRequestHeader = false; 44 | options.IncludeInResponse = true; 45 | options.RequestHeader = "My-Custom-Correlation-Id"; 46 | options.ResponseHeader = "X-Correlation-Id"; 47 | options.UpdateTraceIdentifier = false; 48 | }); 49 | ``` 50 | 51 | ### CorrelationIdOptions 52 | 53 | **BREAKING CHANGES** 54 | 55 | * `DefaultHeader` renamed to `RequestHeader` 56 | * `UseGuidForCorrelationId` removed as this is now controlled by the registered `ICorrelationIdProvider`. 57 | 58 | *New Options* 59 | 60 | * Added `ResponseHeader` - The name of the header to which the Correlation ID is written. This change supports scenarios where it is necessary to read from one header but return the correlation ID using a different header name. Defaults to the same value as the `RequestHeader` unless specifically set. 61 | * Added `IgnoreRequestHeader` - When `true` the incoming correlation ID in the `RequestHeader` is ignored and a new correlation ID is generated. 62 | * Added `EnforceHeader` - Enforces the inclusion of the correlation ID request header. When `true` and a correlation ID header is not included, the request will fail with a 400 Bad Request response. 63 | * Added `AddToLoggingScope` - Add the correlation ID value to the logger scope for all requests. When `true` the value of the correlation ID will be added to the logger scope payload. 64 | * Added `LoggingScopeKey` - The name for the key used when adding the correlation ID to the logger scope. Defaults to 'CorrelationId' 65 | * Added `CorrelationIdGenerator` - A `Func` that returns the correlation ID in cases where no correlation ID is retrieved from the request header. It can be used to customise the correlation ID generation. When set, this function will be used instead of the registered `ICorrelationIdProvider`. 66 | 67 | ### CorrelationContext 68 | 69 | The constructor for this type has been made public (previously internal) to support the creation of `CorrelationContext` instances when unit testing code which depends on the `ICorrelationContextAccessor`. This makes mocking the `ICorrelationContextAccessor` a much easier task. 70 | 71 | ### IApplicationBuilder Extension Methods 72 | 73 | The overloads of the `UseCorrelationId` methods have been removed as options are no longer provided when adding the correlation ID middleware to the pipeline. 74 | 75 | * `UseCorrelationId(string header)` removed. 76 | * `UseCorrelationId(CorrelationIdOptions options)` removed. 77 | 78 | ## 2.1.0 79 | 80 | **Potential breaking changes** 81 | 82 | Unfortunately, despite this being a minor release, a potential breaking change has slipped in. The Create method on the CorrelationContextFactory requires two arguments (previously one). If you are mocking or using this class directly, then this change may affect you. 83 | 84 | * Adds a new option `UpdateTraceIdentifier`: Controls whether the ASP.NET Core TraceIdentifier will be set to match the CorrelationId. The default value is `true`. 85 | * Adds a new option `UseGuidForCorrelationId`: Controls whether a GUID will be used in cases where no correlation ID is retrieved from the request header. The default value is `false`. 86 | 87 | ## 2.0.1 88 | 89 | * Non-breaking change to include the correct project and repo URLs in the NuGet package information. 90 | 91 | ## 2.0.0 92 | 93 | **Includes breaking changes** 94 | 95 | This major release introduces a key requirement of including a `CorrelationContext` which makes it possible to access the CorrelationId in classes that don't automatically have access to the HttpContext in order to retrieve the TraceIdentifier. 96 | 97 | This is a breaking change since the registration of services in now required. An exception will be thrown if the services are not registered prior to calling the middleware. 98 | 99 | Consuming classes can now include a constructor dependency for `ICorrelationContextAccessor` which will enable the retrieval and use of the current `CorrelationContext` when performing logging. 100 | 101 | ## v1.0.1 102 | 103 | * Fix #3 - Avoid setting response header if it is already set 104 | 105 | ## v1.0.0 106 | 107 | * Initial release 108 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | 8 | namespace MvcSample.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | private readonly IHttpClientFactory _httpClientFactory; 21 | 22 | public WeatherForecastController(ILogger logger, IHttpClientFactory httpClientFactory) 23 | { 24 | _logger = logger; 25 | _httpClientFactory = httpClientFactory; 26 | } 27 | 28 | [HttpGet] 29 | public IEnumerable Get() 30 | { 31 | var client = _httpClientFactory.CreateClient("MyClient"); // this client will attach the correlation ID header 32 | 33 | client.GetAsync("https://www.example.com"); 34 | 35 | var rng = new Random(); 36 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 37 | { 38 | Date = DateTime.Now.AddDays(index), 39 | TemperatureC = rng.Next(-20, 55), 40 | Summary = Summaries[rng.Next(Summaries.Length)] 41 | }) 42 | .ToArray(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/DoNothingCorrelationIdProvider.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace MvcSample 5 | { 6 | public class DoNothingCorrelationIdProvider : ICorrelationIdProvider 7 | { 8 | public string GenerateCorrelationId(HttpContext context) 9 | { 10 | return null; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /samples/3.1/MvcSample/MvcSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/NoOpDelegatingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace MvcSample 9 | { 10 | public class NoOpDelegatingHandler : DelegatingHandler 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public NoOpDelegatingHandler(ILogger logger) => _logger = logger; 15 | 16 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 17 | { 18 | if (request.Headers.TryGetValues("X-Correlation-Id", out var headerEnumerable)) 19 | { 20 | _logger.LogInformation("Request has the following correlation ID header {CorrelationId}.", headerEnumerable.FirstOrDefault()); 21 | } 22 | else 23 | { 24 | _logger.LogInformation("Request does not have a correlation ID header."); 25 | } 26 | 27 | var response = new HttpResponseMessage(HttpStatusCode.OK); 28 | 29 | return Task.FromResult(response); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace MvcSample 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:53567", 8 | "sslPort": 44328 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": false, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "MvcSample": { 21 | "commandName": "Project", 22 | "launchBrowser": false, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/ServiceWhichUsesCorrelationContext.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | 3 | namespace MvcSample 4 | { 5 | public class ServiceWhichUsesCorrelationContext 6 | { 7 | private readonly ICorrelationContextAccessor _correlationContextAccessor; 8 | 9 | public ServiceWhichUsesCorrelationContext(ICorrelationContextAccessor correlationContextAccessor) => _correlationContextAccessor = correlationContextAccessor; 10 | 11 | public string DoStuff() 12 | { 13 | var correlationId = _correlationContextAccessor.CorrelationContext.CorrelationId; 14 | 15 | return $"Formatted correlation ID:{correlationId}"; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/Startup.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId; 2 | using CorrelationId.DependencyInjection; 3 | using CorrelationId.HttpClient; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | 10 | namespace MvcSample 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddTransient(); 25 | 26 | services.AddHttpClient("MyClient") 27 | .AddCorrelationIdForwarding() // add the handler to attach the correlation ID to outgoing requests for this named client 28 | .AddHttpMessageHandler(); 29 | 30 | // Example of adding default correlation ID (using the GUID generator) services 31 | // As shown here, options can be configured via the configure delegate overload 32 | services.AddDefaultCorrelationId(options => 33 | { 34 | options.CorrelationIdGenerator = () => "Foo"; 35 | options.AddToLoggingScope = true; 36 | options.EnforceHeader = true; 37 | options.IgnoreRequestHeader = false; 38 | options.IncludeInResponse = true; 39 | options.RequestHeader = "My-Custom-Correlation-Id"; 40 | options.ResponseHeader = "X-Correlation-Id"; 41 | options.UpdateTraceIdentifier = false; 42 | }); 43 | 44 | // Example of registering a custom correlation ID provider 45 | //services.AddCorrelationId().WithCustomProvider(); 46 | 47 | services.AddControllers(); 48 | } 49 | 50 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 51 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 52 | { 53 | if (env.IsDevelopment()) 54 | { 55 | app.UseDeveloperExceptionPage(); 56 | } 57 | 58 | app.UseHttpsRedirection(); 59 | 60 | app.UseCorrelationId(); // adds the correlation ID middleware 61 | 62 | app.UseRouting(); 63 | 64 | app.UseAuthorization(); 65 | 66 | app.UseEndpoints(endpoints => 67 | { 68 | endpoints.MapControllers(); 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MvcSample 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/3.1/MvcSample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/3.1/MvcSampleTests/MvcSampleTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/3.1/MvcSampleTests/ServiceWhichUsesCorrelationContextTests.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId; 2 | using CorrelationId.Abstractions; 3 | using Moq; 4 | using MvcSample; 5 | using Xunit; 6 | 7 | namespace MvcSampleTests 8 | { 9 | public class ServiceWhichUsesCorrelationContextTests 10 | { 11 | [Fact] 12 | public void DoStuff_Test() 13 | { 14 | var mockCorrelationContextAccessor = new Mock(); 15 | mockCorrelationContextAccessor.Setup(x => x.CorrelationContext) 16 | .Returns(new CorrelationContext("ABC", "RequestHeader")); 17 | 18 | var sut = new ServiceWhichUsesCorrelationContext(mockCorrelationContextAccessor.Object); 19 | 20 | var result = sut.DoStuff(); 21 | 22 | Assert.Equal("Formatted correlation ID:ABC", result); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CorrelationId/Abstractions/ICorrelationContextAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace CorrelationId.Abstractions 2 | { 3 | /// 4 | /// Provides access to the for the current request. 5 | /// 6 | public interface ICorrelationContextAccessor 7 | { 8 | /// 9 | /// The for the current request. 10 | /// 11 | CorrelationContext CorrelationContext { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/CorrelationId/Abstractions/ICorrelationContextFactory.cs: -------------------------------------------------------------------------------- 1 | namespace CorrelationId.Abstractions 2 | { 3 | /// 4 | /// A factory for creating and disposing an instance of a . 5 | /// 6 | public interface ICorrelationContextFactory 7 | { 8 | /// 9 | /// Creates a new with the correlation ID set for the current request. 10 | /// 11 | /// The correlation ID to set on the context. 12 | /// /// The header used to hold the correlation ID. 13 | /// A new instance of a . 14 | CorrelationContext Create(string correlationId, string header); 15 | 16 | /// 17 | /// Disposes of the for the current request. 18 | /// 19 | void Dispose(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CorrelationId/Abstractions/ICorrelationIdProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace CorrelationId.Abstractions 4 | { 5 | /// 6 | /// Defines a provider which can be used to generate correlation IDs. 7 | /// 8 | public interface ICorrelationIdProvider 9 | { 10 | /// 11 | /// Generates a correlation ID string for the current request. 12 | /// 13 | /// The of the current request. 14 | /// A string representing the correlation ID. 15 | string GenerateCorrelationId(HttpContext context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationContext.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | using System; 3 | 4 | namespace CorrelationId 5 | { 6 | /// 7 | /// Provides access to per request correlation properties. 8 | /// 9 | public class CorrelationContext 10 | { 11 | /// 12 | /// The default correlation ID is used in cases where the correlation has not been set by the . 13 | /// 14 | public const string DefaultCorrelationId = "Not set"; 15 | 16 | /// 17 | /// Create a instance. 18 | /// 19 | /// The correlation ID on the context. 20 | /// The name of the header from which the Correlation ID was read/written. 21 | /// Thrown if the is null or empty. 22 | public CorrelationContext(string correlationId, string header) 23 | { 24 | correlationId ??= DefaultCorrelationId; 25 | 26 | if (string.IsNullOrEmpty(header)) 27 | throw new ArgumentException("A header must be provided.", nameof(header)); 28 | 29 | CorrelationId = correlationId; 30 | Header = header; 31 | } 32 | 33 | /// 34 | /// The Correlation ID which is applicable to the current request. 35 | /// 36 | public string CorrelationId { get; } 37 | 38 | /// 39 | /// The name of the header from which the Correlation ID was read/written. 40 | /// 41 | public string Header { get; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationContextAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using CorrelationId.Abstractions; 3 | 4 | namespace CorrelationId 5 | { 6 | /// 7 | public class CorrelationContextAccessor : ICorrelationContextAccessor 8 | { 9 | private static AsyncLocal _correlationContext = new AsyncLocal(); 10 | 11 | /// 12 | public CorrelationContext CorrelationContext 13 | { 14 | get => _correlationContext.Value; 15 | set => _correlationContext.Value = value; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationContextFactory.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | 3 | namespace CorrelationId 4 | { 5 | /// 6 | public class CorrelationContextFactory : ICorrelationContextFactory 7 | { 8 | private readonly ICorrelationContextAccessor _correlationContextAccessor; 9 | 10 | /// 11 | /// Initialises a new instance of . 12 | /// 13 | public CorrelationContextFactory() 14 | : this(null) 15 | { } 16 | 17 | /// 18 | /// Initialises a new instance of the class. 19 | /// 20 | /// The through which the will be set. 21 | public CorrelationContextFactory(ICorrelationContextAccessor correlationContextAccessor) 22 | { 23 | _correlationContextAccessor = correlationContextAccessor; 24 | } 25 | 26 | /// 27 | public CorrelationContext Create(string correlationId, string header) 28 | { 29 | var correlationContext = new CorrelationContext(correlationId, header); 30 | 31 | if (_correlationContextAccessor != null) 32 | { 33 | _correlationContextAccessor.CorrelationContext = correlationContext; 34 | } 35 | 36 | return correlationContext; 37 | } 38 | 39 | /// 40 | public void Dispose() 41 | { 42 | if (_correlationContextAccessor != null) 43 | { 44 | _correlationContextAccessor.CorrelationContext = null; 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationId.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | False 6 | Steve Gordon 7 | Steve Gordon | www.stevejgordon.co.uk 8 | CorrelationId Middleware for ASP.NET Core 9 | ASP.NET Core correlation ID middleware for distributed microservices. 10 | © Steve Gordon 2017-2020. All rights reserved. 11 | git 12 | aspnetcore;correlationid 13 | en 14 | 3.0.0 15 | bin\Release\netstandard2.0\CorrelationId.xml 16 | false 17 | MIT 18 | https://github.com/stevejgordon/CorrelationId 19 | https://github.com/stevejgordon/CorrelationId 20 | 8 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationIdExtensions.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | using Microsoft.AspNetCore.Builder; 3 | using System; 4 | 5 | namespace CorrelationId 6 | { 7 | /// 8 | /// Extension methods for the CorrelationIdMiddleware. 9 | /// 10 | public static class CorrelationIdExtensions 11 | { 12 | /// 13 | /// Enables correlation IDs for the request. 14 | /// 15 | /// 16 | /// 17 | public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) 18 | { 19 | if (app == null) 20 | { 21 | throw new ArgumentNullException(nameof(app)); 22 | } 23 | 24 | if (app.ApplicationServices.GetService(typeof(ICorrelationContextFactory)) == null) 25 | { 26 | throw new InvalidOperationException("Unable to find the required services. You must call the appropriate AddCorrelationId/AddDefaultCorrelationId method in ConfigureServices in the application startup code."); 27 | } 28 | 29 | return app.UseMiddleware(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationIdMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.Extensions.Primitives; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using CorrelationId.Abstractions; 10 | 11 | namespace CorrelationId 12 | { 13 | /// 14 | /// Middleware which attempts to reads / creates a Correlation ID that can then be used in logs and 15 | /// passed to upstream requests. 16 | /// 17 | public class CorrelationIdMiddleware 18 | { 19 | private readonly RequestDelegate _next; 20 | private readonly ILogger _logger; 21 | private readonly ICorrelationIdProvider _correlationIdProvider; 22 | private readonly CorrelationIdOptions _options; 23 | 24 | /// 25 | /// Creates a new instance of the CorrelationIdMiddleware. 26 | /// 27 | /// The next middleware in the pipeline. 28 | /// The instance to log to. 29 | /// The configuration options. 30 | /// 31 | public CorrelationIdMiddleware(RequestDelegate next, ILogger logger, IOptions options, ICorrelationIdProvider correlationIdProvider = null) 32 | { 33 | _next = next ?? throw new ArgumentNullException(nameof(next)); 34 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 35 | _correlationIdProvider = correlationIdProvider; 36 | _options = options.Value ?? throw new ArgumentNullException(nameof(options)); 37 | } 38 | 39 | /// 40 | /// Processes a request to synchronise TraceIdentifier and Correlation ID headers. Also creates a 41 | /// for the current request and disposes of it when the request is completing. 42 | /// 43 | /// The for the current request. 44 | /// The which can create a . 45 | public async Task Invoke(HttpContext context, ICorrelationContextFactory correlationContextFactory) 46 | { 47 | Log.CorrelationIdProcessingBegin(_logger); 48 | 49 | if (_correlationIdProvider is null) 50 | { 51 | Log.MissingCorrelationIdProvider(_logger); 52 | 53 | throw new InvalidOperationException("No 'ICorrelationIdProvider' has been registered. You must either add the correlation ID services" + 54 | " using the 'AddDefaultCorrelationId' extension method or you must register a suitable provider using the" + 55 | " 'ICorrelationIdBuilder'."); 56 | } 57 | 58 | var hasCorrelationIdHeader = context.Request.Headers.TryGetValue(_options.RequestHeader, out var cid) && 59 | !StringValues.IsNullOrEmpty(cid); 60 | 61 | if (!hasCorrelationIdHeader && _options.EnforceHeader) 62 | { 63 | Log.EnforcedCorrelationIdHeaderMissing(_logger); 64 | 65 | context.Response.StatusCode = StatusCodes.Status400BadRequest; 66 | await context.Response.WriteAsync($"The '{_options.RequestHeader}' request header is required, but was not found."); 67 | return; 68 | } 69 | 70 | var correlationId = hasCorrelationIdHeader ? cid.FirstOrDefault() : null; 71 | 72 | if (hasCorrelationIdHeader) 73 | { 74 | Log.FoundCorrelationIdHeader(_logger, correlationId); 75 | } 76 | else 77 | { 78 | Log.MissingCorrelationIdHeader(_logger); 79 | } 80 | 81 | if (_options.IgnoreRequestHeader || RequiresGenerationOfCorrelationId(hasCorrelationIdHeader, cid)) 82 | { 83 | correlationId = GenerateCorrelationId(context); 84 | } 85 | 86 | if (!string.IsNullOrEmpty(correlationId) && _options.UpdateTraceIdentifier) 87 | { 88 | Log.UpdatingTraceIdentifier(_logger); 89 | 90 | context.TraceIdentifier = correlationId; 91 | } 92 | 93 | Log.CreatingCorrelationContext(_logger); 94 | correlationContextFactory.Create(correlationId, _options.RequestHeader); 95 | 96 | if (_options.IncludeInResponse && !string.IsNullOrEmpty(correlationId)) 97 | { 98 | // apply the correlation ID to the response header for client side tracking 99 | context.Response.OnStarting(() => 100 | { 101 | if (!context.Response.Headers.ContainsKey(_options.ResponseHeader)) 102 | { 103 | Log.WritingCorrelationIdResponseHeader(_logger, _options.ResponseHeader, correlationId); 104 | context.Response.Headers.Add(_options.ResponseHeader, correlationId); 105 | } 106 | 107 | return Task.CompletedTask; 108 | }); 109 | } 110 | 111 | if (_options.AddToLoggingScope && !string.IsNullOrEmpty(_options.LoggingScopeKey) && !string.IsNullOrEmpty(correlationId)) 112 | { 113 | using (_logger.BeginScope(new Dictionary 114 | { 115 | [_options.LoggingScopeKey] = correlationId 116 | })) 117 | { 118 | Log.CorrelationIdProcessingEnd(_logger, correlationId); 119 | await _next(context); 120 | } 121 | } 122 | else 123 | { 124 | Log.CorrelationIdProcessingEnd(_logger, correlationId); 125 | await _next(context); 126 | } 127 | 128 | Log.DisposingCorrelationContext(_logger); 129 | correlationContextFactory.Dispose(); 130 | } 131 | 132 | private static bool RequiresGenerationOfCorrelationId(bool idInHeader, StringValues idFromHeader) => 133 | !idInHeader || StringValues.IsNullOrEmpty(idFromHeader); 134 | 135 | private string GenerateCorrelationId(HttpContext ctx) 136 | { 137 | string correlationId; 138 | 139 | if (_options.CorrelationIdGenerator is object) 140 | { 141 | correlationId = _options.CorrelationIdGenerator(); 142 | Log.GeneratedHeaderUsingGeneratorFunction(_logger, correlationId); 143 | return correlationId; 144 | } 145 | 146 | correlationId = _correlationIdProvider.GenerateCorrelationId(ctx); 147 | Log.GeneratedHeaderUsingProvider(_logger, correlationId, _correlationIdProvider.GetType()); 148 | return correlationId; 149 | } 150 | 151 | internal static class EventIds 152 | { 153 | public static readonly EventId CorrelationIdProcessingBegin = new EventId(100, "CorrelationIdProcessingBegin"); 154 | public static readonly EventId CorrelationIdProcessingEnd = new EventId(101, "CorrelationIdProcessingEnd"); 155 | 156 | public static readonly EventId MissingCorrelationIdProvider = new EventId(103, "MissingCorrelationIdProvider"); 157 | public static readonly EventId EnforcedCorrelationIdHeaderMissing = new EventId(104, "EnforcedCorrelationIdHeaderMissing"); 158 | public static readonly EventId FoundCorrelationIdHeader = new EventId(105, "EnforcedCorrelationIdHeaderMissing"); 159 | public static readonly EventId MissingCorrelationIdHeader = new EventId(106, "MissingCorrelationIdHeader"); 160 | 161 | public static readonly EventId GeneratedHeaderUsingGeneratorFunction = new EventId(107, "GeneratedHeaderUsingGeneratorFunction"); 162 | public static readonly EventId GeneratedHeaderUsingProvider = new EventId(108, "GeneratedHeaderUsingProvider"); 163 | 164 | public static readonly EventId UpdatingTraceIdentifier = new EventId(109, "UpdatingTraceIdentifier"); 165 | public static readonly EventId CreatingCorrelationContext = new EventId(110, "CreatingCorrelationContext"); 166 | public static readonly EventId DisposingCorrelationContext = new EventId(111, "DisposingCorrelationContext"); 167 | public static readonly EventId WritingCorrelationIdResponseHeader = new EventId(112, "WritingCorrelationIdResponseHeader"); 168 | } 169 | 170 | private static class Log 171 | { 172 | private static readonly Action _correlationIdProcessingBegin = LoggerMessage.Define( 173 | LogLevel.Debug, 174 | EventIds.CorrelationIdProcessingBegin, 175 | "Running correlation ID processing"); 176 | 177 | private static readonly Action _correlationIdProcessingEnd = LoggerMessage.Define( 178 | LogLevel.Debug, 179 | EventIds.CorrelationIdProcessingEnd, 180 | "Correlation ID processing was completed with a final correlation ID {CorrelationId}"); 181 | 182 | private static readonly Action _missingCorrelationIdProvider = LoggerMessage.Define( 183 | LogLevel.Error, 184 | EventIds.MissingCorrelationIdProvider, 185 | "Correlation ID middleware was called when no ICorrelationIdProvider had been configured"); 186 | 187 | private static readonly Action _enforcedCorrelationIdHeaderMissing = LoggerMessage.Define( 188 | LogLevel.Warning, 189 | EventIds.EnforcedCorrelationIdHeaderMissing, 190 | "Correlation ID header is enforced but no Correlation ID was not found in the request headers"); 191 | 192 | private static readonly Action _foundCorrelationIdHeader = LoggerMessage.Define( 193 | LogLevel.Information, 194 | EventIds.FoundCorrelationIdHeader, 195 | "Correlation ID {CorrelationId} was found in the request headers"); 196 | 197 | private static readonly Action _missingCorrelationIdHeader = LoggerMessage.Define( 198 | LogLevel.Information, 199 | EventIds.MissingCorrelationIdHeader, 200 | "No correlation ID was found in the request headers"); 201 | 202 | private static readonly Action _generatedHeaderUsingGeneratorFunction = LoggerMessage.Define( 203 | LogLevel.Debug, 204 | EventIds.GeneratedHeaderUsingGeneratorFunction, 205 | "Generated a correlation ID {CorrelationId} using the configured generator function"); 206 | 207 | private static readonly Action _generatedHeaderUsingProvider = LoggerMessage.Define( 208 | LogLevel.Debug, 209 | EventIds.GeneratedHeaderUsingProvider, 210 | "Generated a correlation ID {CorrelationId} using the {Type} provider"); 211 | 212 | private static readonly Action _updatingTraceIdentifier = LoggerMessage.Define( 213 | LogLevel.Debug, 214 | EventIds.UpdatingTraceIdentifier, 215 | "Updating the TraceIdentifier value on the HttpContext"); 216 | 217 | private static readonly Action _creatingCorrelationContext = LoggerMessage.Define( 218 | LogLevel.Debug, 219 | EventIds.CreatingCorrelationContext, 220 | "Creating the correlation context for this request"); 221 | 222 | private static readonly Action _disposingCorrelationContext = LoggerMessage.Define( 223 | LogLevel.Debug, 224 | EventIds.DisposingCorrelationContext, 225 | "Disposing the correlation context for this request"); 226 | 227 | private static readonly Action _writingCorrelationIdResponseHeader = LoggerMessage.Define( 228 | LogLevel.Debug, 229 | EventIds.WritingCorrelationIdResponseHeader, 230 | "Writing correlation ID response header {ResponseHeader} with value {CorrelationId}"); 231 | 232 | public static void CorrelationIdProcessingBegin(ILogger logger) 233 | { 234 | if(logger.IsEnabled(LogLevel.Debug)) _correlationIdProcessingBegin(logger, null); 235 | } 236 | 237 | public static void CorrelationIdProcessingEnd(ILogger logger, string correlationId) 238 | { 239 | if (logger.IsEnabled(LogLevel.Debug)) _correlationIdProcessingEnd(logger, correlationId, null); 240 | } 241 | 242 | public static void MissingCorrelationIdProvider(ILogger logger) => _missingCorrelationIdProvider(logger, null); 243 | 244 | public static void EnforcedCorrelationIdHeaderMissing(ILogger logger) => _enforcedCorrelationIdHeaderMissing(logger, null); 245 | 246 | public static void FoundCorrelationIdHeader(ILogger logger, string correlationId) => _foundCorrelationIdHeader(logger, correlationId, null); 247 | 248 | public static void MissingCorrelationIdHeader(ILogger logger) => _missingCorrelationIdHeader(logger, null); 249 | 250 | public static void GeneratedHeaderUsingGeneratorFunction(ILogger logger, string correlationId) => _generatedHeaderUsingGeneratorFunction(logger, correlationId, null); 251 | 252 | public static void GeneratedHeaderUsingProvider(ILogger logger, string correlationId, Type type) => _generatedHeaderUsingProvider(logger, correlationId, type, null); 253 | 254 | public static void UpdatingTraceIdentifier(ILogger logger) => _updatingTraceIdentifier(logger, null); 255 | 256 | public static void CreatingCorrelationContext(ILogger logger) => _creatingCorrelationContext(logger, null); 257 | 258 | public static void DisposingCorrelationContext(ILogger logger) => _disposingCorrelationContext(logger, null); 259 | 260 | public static void WritingCorrelationIdResponseHeader(ILogger logger, string headerName, string correlationId) => _writingCorrelationIdResponseHeader(logger, headerName, correlationId, null); 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /src/CorrelationId/CorrelationIdOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CorrelationId.Abstractions; 3 | 4 | namespace CorrelationId 5 | { 6 | /// 7 | /// Options for correlation IDs. 8 | /// 9 | public class CorrelationIdOptions 10 | { 11 | /// 12 | /// The default header used for correlation ID. 13 | /// 14 | public const string DefaultHeader = "X-Correlation-ID"; 15 | 16 | /// 17 | /// The default logger scope key for correlation ID logging. 18 | /// 19 | public const string LoggerScopeKey = "CorrelationId"; 20 | 21 | /// 22 | /// The name of the header from which the Correlation ID is read from the request. 23 | /// 24 | public string RequestHeader { get; set; } = DefaultHeader; 25 | 26 | private string _responseHeader = null; 27 | 28 | /// 29 | /// The name of the header to which the Correlation ID is written for the response. 30 | /// 31 | public string ResponseHeader { get => _responseHeader ?? RequestHeader; set => _responseHeader = value; } 32 | 33 | /// 34 | /// 35 | /// Ignore request header. 36 | /// When true, the correlation ID for the current request ignores the correlation ID header value on the request. 37 | /// 38 | /// Default: false 39 | /// 40 | public bool IgnoreRequestHeader { get; set; } = false; 41 | 42 | /// 43 | /// 44 | /// Enforce the inclusion of the correlation ID request header. 45 | /// When true and a correlation ID header is not included, the request will fail with a 400 Bad Request response. 46 | /// 47 | /// Default: false 48 | /// 49 | public bool EnforceHeader { get; set; } = false; 50 | 51 | /// 52 | /// 53 | /// Add the correlation ID value to the logger scope for all requests. 54 | /// When true the value of the correlation ID will be added to the logger scope payload. 55 | /// 56 | /// Default: false 57 | /// 58 | public bool AddToLoggingScope { get; set; } = false; 59 | 60 | /// 61 | /// 62 | /// The name for the key used when adding the correlation ID to the logger scope. 63 | /// 64 | /// Default: 'CorrelationId' 65 | /// 66 | public string LoggingScopeKey { get; set; } = LoggerScopeKey; 67 | 68 | /// 69 | /// 70 | /// Controls whether the correlation ID is returned in the response headers. 71 | /// 72 | /// Default: true 73 | /// 74 | public bool IncludeInResponse { get; set; } = true; 75 | 76 | /// 77 | /// 78 | /// Controls whether the ASP.NET Core TraceIdentifier will be set to match the CorrelationId. 79 | /// 80 | /// Default: false 81 | /// 82 | public bool UpdateTraceIdentifier { get; set; } = false; 83 | 84 | /// 85 | /// A function that returns the correlation ID in cases where no correlation ID is retrieved from the request header. It can be used to customise the correlation ID generation. 86 | /// When set, this function will be used instead of the registered . 87 | /// 88 | public Func CorrelationIdGenerator { get; set; } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/CorrelationId/DependencyInjection/CorrelationIdBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace CorrelationId.DependencyInjection 4 | { 5 | /// 6 | internal class CorrelationIdBuilder : ICorrelationIdBuilder 7 | { 8 | public CorrelationIdBuilder(IServiceCollection services) 9 | { 10 | Services = services; 11 | } 12 | 13 | /// 14 | public IServiceCollection Services { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/CorrelationId/DependencyInjection/CorrelationIdBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | using CorrelationId.Providers; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace CorrelationId.DependencyInjection 8 | { 9 | /// 10 | /// Provides basic extension methods for configuring the correlation ID provider in an . 11 | /// 12 | public static class CorrelationIdBuilderExtensions 13 | { 14 | private const string MultipleProviderExceptionMessage = "A provider has already been registered. Only a single provider may be registered."; 15 | 16 | /// 17 | /// Clear the existing if one has been registered. 18 | /// 19 | /// The . 20 | /// A reference to this instance after the operation has completed. 21 | /// Thrown if the parameter is null. 22 | public static ICorrelationIdBuilder ClearProvider(this ICorrelationIdBuilder builder) 23 | { 24 | if (builder is null) 25 | { 26 | throw new ArgumentNullException(nameof(builder)); 27 | } 28 | 29 | builder.Services.RemoveAll(); 30 | 31 | return builder; 32 | } 33 | 34 | /// 35 | /// Registers the for use when generating correlation IDs. 36 | /// 37 | /// The . 38 | /// A reference to this instance after the operation has completed. 39 | /// Thrown if the parameter is null. 40 | /// Thrown if a has already been registered. 41 | public static ICorrelationIdBuilder WithGuidProvider(this ICorrelationIdBuilder builder) 42 | { 43 | if (builder is null) 44 | { 45 | throw new ArgumentNullException(nameof(builder)); 46 | } 47 | 48 | if (builder.Services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 49 | { 50 | throw new InvalidOperationException(MultipleProviderExceptionMessage); 51 | } 52 | 53 | builder.Services.TryAddSingleton(); 54 | 55 | return builder; 56 | } 57 | 58 | /// 59 | /// Registers the for use when generating correlation IDs. 60 | /// 61 | /// The . 62 | /// A reference to this instance after the operation has completed. 63 | /// Thrown if the parameter is null. 64 | /// Thrown if a has already been registered. 65 | public static ICorrelationIdBuilder WithTraceIdentifierProvider(this ICorrelationIdBuilder builder) 66 | { 67 | if (builder is null) 68 | { 69 | throw new ArgumentNullException(nameof(builder)); 70 | } 71 | 72 | if (builder.Services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 73 | { 74 | throw new InvalidOperationException("A provider has already been added."); 75 | } 76 | 77 | builder.Services.TryAddSingleton(); 78 | 79 | return builder; 80 | } 81 | 82 | /// 83 | /// Registers an existing instance of a custom . 84 | /// 85 | /// The . 86 | /// The instance to register. 87 | /// A reference to this instance after the operation has completed. 88 | /// Thrown if the parameter is null. 89 | /// Thrown if a has already been registered. 90 | public static ICorrelationIdBuilder WithCustomProvider(this ICorrelationIdBuilder builder, ICorrelationIdProvider provider) 91 | { 92 | if (builder is null) 93 | { 94 | throw new ArgumentNullException(nameof(builder)); 95 | } 96 | 97 | if (provider is null) 98 | { 99 | throw new ArgumentNullException(nameof(provider)); 100 | } 101 | 102 | if (builder.Services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 103 | { 104 | throw new InvalidOperationException("A provider has already been added."); 105 | } 106 | 107 | builder.Services.TryAddSingleton(provider); 108 | 109 | return builder; 110 | } 111 | 112 | /// 113 | /// Registers a custom . 114 | /// 115 | /// The implementation type. 116 | /// The . 117 | /// A reference to this instance after the operation has completed. 118 | /// Thrown if the parameter is null. 119 | /// Thrown if a has already been registered. 120 | public static ICorrelationIdBuilder WithCustomProvider(this ICorrelationIdBuilder builder) where T : class, ICorrelationIdProvider 121 | { 122 | if (builder is null) 123 | { 124 | throw new ArgumentNullException(nameof(builder)); 125 | } 126 | 127 | if (builder.Services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 128 | { 129 | throw new InvalidOperationException("A provider has already been added."); 130 | } 131 | 132 | builder.Services.TryAddSingleton(); 133 | 134 | return builder; 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /src/CorrelationId/DependencyInjection/CorrelationIdServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using CorrelationId.Abstractions; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | 7 | namespace CorrelationId.DependencyInjection 8 | { 9 | /// 10 | /// Extensions on the to register the correlation ID services. 11 | /// 12 | public static class CorrelationIdServiceCollectionExtensions 13 | { 14 | /// 15 | /// Adds required services to support the Correlation ID functionality to the . 16 | /// 17 | /// 18 | /// This operation is idempotent - multiple invocations will still only result in a single 19 | /// instance of the required services in the . It can be invoked 20 | /// multiple times in order to get access to the in multiple places. 21 | /// 22 | /// The to add the correlation ID services to. 23 | /// An instance of which to be used to configure correlation ID providers and options. 24 | public static ICorrelationIdBuilder AddCorrelationId(this IServiceCollection services) 25 | { 26 | if (services is null) 27 | { 28 | throw new ArgumentNullException(nameof(services)); 29 | } 30 | 31 | services.TryAddSingleton(); 32 | services.TryAddTransient(); 33 | 34 | return new CorrelationIdBuilder(services); 35 | } 36 | 37 | /// 38 | /// Adds required services to support the Correlation ID functionality to the . 39 | /// 40 | /// 41 | /// This operation is idempotent - multiple invocations will still only result in a single 42 | /// instance of the required services in the . It can be invoked 43 | /// multiple times in order to get access to the in multiple places. 44 | /// 45 | /// The implementation type. 46 | /// The to add the correlation ID services to. 47 | /// An instance of which to be used to configure correlation ID providers and options. 48 | public static ICorrelationIdBuilder AddCorrelationId(this IServiceCollection services) where T : class, ICorrelationIdProvider 49 | { 50 | if (services is null) 51 | { 52 | throw new ArgumentNullException(nameof(services)); 53 | } 54 | 55 | if (services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 56 | { 57 | throw new InvalidOperationException("A provider has already been added."); 58 | } 59 | 60 | var builder = AddCorrelationId(services); 61 | 62 | builder.Services.TryAddSingleton(); 63 | 64 | return builder; 65 | } 66 | 67 | /// 68 | /// Adds required services to support the Correlation ID functionality to the . 69 | /// 70 | /// 71 | /// This operation is idempotent - multiple invocations will still only result in a single 72 | /// instance of the required services in the . It can be invoked 73 | /// multiple times in order to get access to the in multiple places. 74 | /// 75 | /// The to add the correlation ID services to. 76 | /// The to configure the provided . 77 | /// An instance of which to be used to configure correlation ID providers and options. 78 | public static ICorrelationIdBuilder AddCorrelationId(this IServiceCollection services, Action configure) 79 | { 80 | if (services is null) 81 | { 82 | throw new ArgumentNullException(nameof(services)); 83 | } 84 | 85 | if (configure is null) 86 | { 87 | throw new ArgumentNullException(nameof(configure)); 88 | } 89 | 90 | services.Configure(configure); 91 | 92 | return services.AddCorrelationId(); 93 | } 94 | 95 | /// 96 | /// Adds required services to support the Correlation ID functionality to the . 97 | /// 98 | /// 99 | /// This operation is idempotent - multiple invocations will still only result in a single 100 | /// instance of the required services in the . It can be invoked 101 | /// multiple times in order to get access to the in multiple places. 102 | /// 103 | /// The implementation type. 104 | /// The to add the correlation ID services to. 105 | /// The to configure the provided . 106 | /// An instance of which to be used to configure correlation ID providers and options. 107 | public static ICorrelationIdBuilder AddCorrelationId(this IServiceCollection services, Action configure) where T : class, ICorrelationIdProvider 108 | { 109 | if (services is null) 110 | { 111 | throw new ArgumentNullException(nameof(services)); 112 | } 113 | 114 | if (services.Any(x => x.ServiceType == typeof(ICorrelationIdProvider))) 115 | { 116 | throw new InvalidOperationException("A provider has already been added."); 117 | } 118 | 119 | if (configure is null) 120 | { 121 | throw new ArgumentNullException(nameof(configure)); 122 | } 123 | 124 | services.Configure(configure); 125 | 126 | return services.AddCorrelationId(); 127 | } 128 | 129 | /// 130 | /// Adds required services to support the Correlation ID functionality to the . 131 | /// 132 | /// 133 | /// This operation is may only be called once to avoid exceptions from attempting to add the default multiple 134 | /// times. 135 | /// 136 | /// Thrown if this is called multiple times. 137 | /// The to add the correlation ID services to. 138 | /// A reference to this instance after the operation has completed. 139 | public static IServiceCollection AddDefaultCorrelationId(this IServiceCollection services) 140 | { 141 | if (services is null) 142 | { 143 | throw new ArgumentNullException(nameof(services)); 144 | } 145 | 146 | services.AddCorrelationId().WithGuidProvider(); 147 | 148 | return services; 149 | } 150 | 151 | /// 152 | /// Adds required services to support the Correlation ID functionality to the . 153 | /// 154 | /// 155 | /// This operation is may only be called once to avoid exceptions from attempting to add the default multiple 156 | /// times. 157 | /// 158 | /// Thrown if this is called multiple times. 159 | /// The to add the correlation ID services to. 160 | /// The to configure the provided . 161 | /// A reference to this instance after the operation has completed. 162 | public static IServiceCollection AddDefaultCorrelationId(this IServiceCollection services, Action configure) 163 | { 164 | if (services is null) 165 | { 166 | throw new ArgumentNullException(nameof(services)); 167 | } 168 | 169 | if (configure is null) 170 | { 171 | throw new ArgumentNullException(nameof(configure)); 172 | } 173 | 174 | services.AddDefaultCorrelationId(); 175 | 176 | services.Configure(configure); 177 | 178 | return services; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/CorrelationId/DependencyInjection/ICorrelationIdBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace CorrelationId.DependencyInjection 4 | { 5 | /// 6 | /// A builder used to configure the correlation ID services. 7 | /// 8 | public interface ICorrelationIdBuilder 9 | { 10 | /// 11 | /// Gets the into which the correlation ID services will be registered. 12 | /// 13 | IServiceCollection Services { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CorrelationId/HttpClient/CorrelationIdHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using CorrelationId.Abstractions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace CorrelationId.HttpClient 7 | { 8 | /// 9 | /// A which adds the correlation ID header from the onto outgoing HTTP requests. 10 | /// 11 | internal sealed class CorrelationIdHandler : DelegatingHandler 12 | { 13 | private readonly ICorrelationContextAccessor _correlationContextAccessor; 14 | 15 | public CorrelationIdHandler(ICorrelationContextAccessor correlationContextAccessor) => _correlationContextAccessor = correlationContextAccessor; 16 | 17 | /// 18 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 19 | { 20 | string correlationId = _correlationContextAccessor?.CorrelationContext?.CorrelationId; 21 | if (!string.IsNullOrEmpty(correlationId) 22 | && !request.Headers.Contains(_correlationContextAccessor.CorrelationContext.Header)) 23 | { 24 | request.Headers.Add(_correlationContextAccessor.CorrelationContext.Header, _correlationContextAccessor.CorrelationContext.CorrelationId); 25 | } 26 | 27 | return base.SendAsync(request, cancellationToken); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CorrelationId/HttpClient/HttpClientBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace CorrelationId.HttpClient 6 | { 7 | /// 8 | /// Extension methods for . 9 | /// 10 | public static class HttpClientBuilderExtensions 11 | { 12 | /// 13 | /// Adds a handler which forwards the correlation ID by attaching it to the headers on outgoing requests. 14 | /// 15 | /// 16 | /// The header name will match the name of the incoming request header. 17 | /// 18 | /// The . 19 | /// A reference to this instance after the operation has completed. 20 | public static IHttpClientBuilder AddCorrelationIdForwarding(this IHttpClientBuilder builder) 21 | { 22 | if (builder is null) 23 | { 24 | throw new ArgumentNullException(nameof(builder)); 25 | } 26 | 27 | builder.Services.TryAddTransient(); 28 | builder.AddHttpMessageHandler(); 29 | 30 | return builder; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/CorrelationId/Providers/GuidCorrelationIdProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CorrelationId.Abstractions; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace CorrelationId.Providers 6 | { 7 | /// 8 | /// Generates a correlation ID using a new GUID. 9 | /// 10 | public class GuidCorrelationIdProvider : ICorrelationIdProvider 11 | { 12 | /// 13 | public string GenerateCorrelationId(HttpContext ctx) => Guid.NewGuid().ToString(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/CorrelationId/Providers/TraceIdCorrelationIdProvider.cs: -------------------------------------------------------------------------------- 1 | using CorrelationId.Abstractions; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace CorrelationId.Providers 5 | { 6 | /// 7 | /// Sets the correlation ID to match the TraceIdentifier set on the . 8 | /// 9 | public class TraceIdCorrelationIdProvider : ICorrelationIdProvider 10 | { 11 | /// 12 | public string GenerateCorrelationId(HttpContext ctx) => ctx.TraceIdentifier; 13 | } 14 | } -------------------------------------------------------------------------------- /test/CorrelationId.Tests/CorrelationId.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 8 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/CorrelationId.Tests/CorrelationIdMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.TestHost; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.DependencyInjection.Extensions; 12 | using Xunit; 13 | using System; 14 | using System.Net; 15 | using CorrelationId.Abstractions; 16 | using CorrelationId.DependencyInjection; 17 | 18 | namespace CorrelationId.Tests 19 | { 20 | public class CorrelationIdMiddlewareTests 21 | { 22 | [Fact] 23 | public async Task Throws_WhenCorrelationIdProviderIsNotRegistered() 24 | { 25 | Exception exception = null; 26 | 27 | var builder = new WebHostBuilder() 28 | .Configure(app => 29 | { 30 | app.Use(async (ctx, next) => 31 | { 32 | try 33 | { 34 | await next.Invoke(); 35 | } 36 | catch (Exception e) 37 | { 38 | exception = e; 39 | } 40 | }); 41 | 42 | app.UseCorrelationId(); 43 | }) 44 | .ConfigureServices(sc => sc.AddCorrelationId()); 45 | 46 | using var server = new TestServer(builder); 47 | 48 | await server.CreateClient().GetAsync(""); 49 | 50 | Assert.NotNull(exception); 51 | Assert.Equal(typeof(InvalidOperationException), exception.GetType()); 52 | } 53 | 54 | [Fact] 55 | public async Task DoesNotThrow_WhenCorrelationIdProviderIsRegistered() 56 | { 57 | Exception exception = null; 58 | 59 | var builder = new WebHostBuilder() 60 | .Configure(app => 61 | { 62 | app.Use(async (ctx, next) => 63 | { 64 | try 65 | { 66 | await next.Invoke(); 67 | } 68 | catch (Exception e) 69 | { 70 | exception = e; 71 | } 72 | }); 73 | 74 | app.UseCorrelationId(); 75 | }) 76 | .ConfigureServices(sc => sc.AddDefaultCorrelationId()); 77 | 78 | using var server = new TestServer(builder); 79 | 80 | await server.CreateClient().GetAsync(""); 81 | 82 | Assert.Null(exception); 83 | } 84 | 85 | [Fact] 86 | public async Task ReturnsBadRequest_WhenEnforceOptionSetToTrue_AndNoExistingHeaderIsSent() 87 | { 88 | var builder = new WebHostBuilder() 89 | .Configure(app => app.UseCorrelationId()) 90 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.EnforceHeader = true)); 91 | 92 | using var server = new TestServer(builder); 93 | 94 | var response = await server.CreateClient().GetAsync(""); 95 | 96 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 97 | } 98 | 99 | [Fact] 100 | public async Task DoesNotReturnBadRequest_WhenEnforceOptionSetToFalse_AndNoExistingHeaderIsSent() 101 | { 102 | var builder = new WebHostBuilder() 103 | .Configure(app => app.UseCorrelationId()) 104 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.EnforceHeader = false)); 105 | 106 | using var server = new TestServer(builder); 107 | 108 | var response = await server.CreateClient().GetAsync(""); 109 | 110 | Assert.NotEqual(HttpStatusCode.BadRequest, response.StatusCode); 111 | } 112 | 113 | [Fact] 114 | public async Task IgnoresRequestHeader_WhenOptionIsTrue() 115 | { 116 | var builder = new WebHostBuilder() 117 | .Configure(app => app.UseCorrelationId()) 118 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => { options.IgnoreRequestHeader = true; options.IncludeInResponse = true; }));; 119 | 120 | using var server = new TestServer(builder); 121 | 122 | var request = new HttpRequestMessage(); 123 | request.Headers.Add(CorrelationIdOptions.DefaultHeader, "ABC123"); 124 | 125 | var response = await server.CreateClient().SendAsync(request); 126 | 127 | var header = response.Headers.GetValues(CorrelationIdOptions.DefaultHeader).FirstOrDefault(); 128 | 129 | Assert.NotEqual("ABC123", header); 130 | } 131 | 132 | [Fact] 133 | public async Task DoesNotIgnoresRequestHeader_WhenOptionIsFalse() 134 | { 135 | var builder = new WebHostBuilder() 136 | .Configure(app => app.UseCorrelationId()) 137 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => { options.IgnoreRequestHeader = false; options.IncludeInResponse = true; })); ; 138 | 139 | using var server = new TestServer(builder); 140 | 141 | var request = new HttpRequestMessage(); 142 | request.Headers.Add(CorrelationIdOptions.DefaultHeader, "ABC123"); 143 | 144 | var response = await server.CreateClient().SendAsync(request); 145 | 146 | var header = response.Headers.GetValues(CorrelationIdOptions.DefaultHeader).FirstOrDefault(); 147 | 148 | Assert.Equal("ABC123", header); 149 | } 150 | 151 | [Fact] 152 | public async Task ReturnsCorrelationIdInResponseHeader_WhenOptionSetToTrue() 153 | { 154 | var builder = new WebHostBuilder() 155 | .Configure(app => app.UseCorrelationId()) 156 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.IncludeInResponse = true)); 157 | 158 | using var server = new TestServer(builder); 159 | 160 | var response = await server.CreateClient().GetAsync(""); 161 | 162 | var header = response.Headers.GetValues(CorrelationIdOptions.DefaultHeader); 163 | 164 | Assert.NotNull(header); 165 | } 166 | 167 | [Fact] 168 | public async Task DoesNotThrowException_WhenOptionSetToTrue_IfHeaderIsAlreadySet() 169 | { 170 | Exception exception = null; 171 | 172 | var builder = new WebHostBuilder() 173 | .Configure(app => 174 | { 175 | app.Use(async (ctx, next) => 176 | { 177 | try 178 | { 179 | await next.Invoke(); 180 | } 181 | catch (Exception e) 182 | { 183 | exception = e; 184 | } 185 | }); 186 | 187 | app.UseCorrelationId(); 188 | app.UseCorrelationId(); // header will already be set on this second use of the middleware 189 | }) 190 | .ConfigureServices(sc => sc.AddDefaultCorrelationId()); 191 | 192 | using var server = new TestServer(builder); 193 | 194 | await server.CreateClient().GetAsync(""); 195 | 196 | Assert.Null(exception); 197 | } 198 | 199 | [Fact] 200 | public async Task DoesNotReturnCorrelationIdInResponseHeader_WhenIncludeInResponseIsFalse() 201 | { 202 | string header = null; 203 | 204 | var builder = new WebHostBuilder() 205 | .Configure(app => app.UseCorrelationId()) 206 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => 207 | { 208 | options.IncludeInResponse = false; 209 | header = options.RequestHeader; 210 | })); 211 | 212 | using var server = new TestServer(builder); 213 | 214 | var response = await server.CreateClient().GetAsync(""); 215 | 216 | var headerExists = response.Headers.TryGetValues(header, out IEnumerable _); 217 | 218 | Assert.False(headerExists); 219 | } 220 | 221 | [Fact] 222 | public async Task CorrelationIdHeaderFieldName_MatchesHeaderOption() 223 | { 224 | const string customHeader = "X-Test-RequestHeader"; 225 | 226 | var builder = new WebHostBuilder() 227 | .Configure(app => app.UseCorrelationId()) 228 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.ResponseHeader = customHeader)); 229 | 230 | using var server = new TestServer(builder); 231 | 232 | var response = await server.CreateClient().GetAsync(""); 233 | 234 | var header = response.Headers.GetValues(customHeader); 235 | 236 | Assert.NotNull(header); 237 | } 238 | 239 | [Fact] 240 | public async Task CorrelationId_SetToCorrelationIdFromRequestHeader() 241 | { 242 | var expectedHeaderName = new CorrelationIdOptions().RequestHeader; 243 | const string expectedHeaderValue = "123456"; 244 | 245 | var builder = new WebHostBuilder() 246 | .Configure(app => app.UseCorrelationId()) 247 | .ConfigureServices(sc => sc.AddDefaultCorrelationId()); 248 | 249 | using var server = new TestServer(builder); 250 | 251 | var request = new HttpRequestMessage(); 252 | request.Headers.Add(expectedHeaderName, expectedHeaderValue); 253 | 254 | var response = await server.CreateClient().SendAsync(request); 255 | 256 | var header = response.Headers.GetValues(expectedHeaderName); 257 | 258 | Assert.Single(header, expectedHeaderValue); 259 | } 260 | 261 | [Fact] 262 | public async Task CorrelationId_SetToGuid_RegisteredWithAddDefaultCorrelationId() 263 | { 264 | var builder = new WebHostBuilder() 265 | .Configure(app => app.UseCorrelationId()) 266 | .ConfigureServices(sc => sc.AddDefaultCorrelationId()); 267 | 268 | using var server = new TestServer(builder); 269 | 270 | var response = await server.CreateClient().GetAsync(""); 271 | 272 | var header = response.Headers.GetValues(new CorrelationIdOptions().RequestHeader); 273 | 274 | var isGuid = Guid.TryParse(header.FirstOrDefault(), out _); 275 | 276 | Assert.True(isGuid); 277 | } 278 | 279 | [Fact] 280 | public async Task CorrelationId_SetToCustomGenerator_WhenCorrelationIdGeneratorIsSet() 281 | { 282 | var builder = new WebHostBuilder() 283 | .Configure(app => app.UseCorrelationId()) 284 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.CorrelationIdGenerator = () => "Foo")); 285 | 286 | using var server = new TestServer(builder); 287 | 288 | var response = await server.CreateClient().GetAsync(""); 289 | 290 | var header = response.Headers.GetValues(new CorrelationIdOptions().RequestHeader); 291 | 292 | var correlationId = header.FirstOrDefault(); 293 | 294 | Assert.Equal("Foo", correlationId); 295 | } 296 | 297 | [Fact] 298 | public async Task CorrelationId_NotSetToGuid_WhenUsingTheTraceIdentifierProvider() 299 | { 300 | var builder = new WebHostBuilder() 301 | .Configure(app => app.UseCorrelationId()) 302 | .ConfigureServices(sc => sc.AddCorrelationId().WithTraceIdentifierProvider()); 303 | 304 | using var server = new TestServer(builder); 305 | 306 | var response = await server.CreateClient().GetAsync(""); 307 | 308 | var header = response.Headers.GetValues(new CorrelationIdOptions().RequestHeader); 309 | 310 | var isGuid = Guid.TryParse(header.FirstOrDefault(), out _); 311 | 312 | Assert.False(isGuid); 313 | } 314 | 315 | [Fact] 316 | public async Task CorrelationId_SetUsingCustomProvider_WhenCustomProviderIsRegistered() 317 | { 318 | var builder = new WebHostBuilder() 319 | .Configure(app => app.UseCorrelationId()) 320 | .ConfigureServices(sc => sc.AddCorrelationId().WithCustomProvider()); 321 | 322 | using var server = new TestServer(builder); 323 | 324 | var response = await server.CreateClient().GetAsync(""); 325 | 326 | var header = response.Headers.GetValues(new CorrelationIdOptions().RequestHeader); 327 | 328 | Assert.Equal(TestCorrelationIdProvider.FixedCorrelationId, header.FirstOrDefault()); 329 | } 330 | 331 | [Fact] 332 | public async Task CorrelationId_ReturnedCorrectlyFromSingletonService() 333 | { 334 | var expectedHeaderName = new CorrelationIdOptions().RequestHeader; 335 | 336 | var builder = new WebHostBuilder() 337 | .Configure(app => 338 | { 339 | app.UseCorrelationId(); 340 | app.Run(async rd => 341 | { 342 | var singleton = app.ApplicationServices.GetService(typeof(SingletonClass)) as SingletonClass; 343 | var scoped = app.ApplicationServices.GetService(typeof(ScopedClass)) as ScopedClass; 344 | 345 | var data = Encoding.UTF8.GetBytes(singleton?.GetCorrelationFromScoped + "|" + scoped?.GetCorrelationFromScoped); 346 | 347 | await rd.Response.Body.WriteAsync(data, 0, data.Length); 348 | }); 349 | }) 350 | .ConfigureServices(sc => 351 | { 352 | sc.AddDefaultCorrelationId(); 353 | sc.TryAddSingleton(); 354 | sc.TryAddScoped(); 355 | }); 356 | 357 | using var server = new TestServer(builder); 358 | 359 | // compare that first request matches the header and the scoped value 360 | var request = new HttpRequestMessage(); 361 | 362 | var response = await server.CreateClient().SendAsync(request); 363 | 364 | var header = response.Headers.GetValues(expectedHeaderName).FirstOrDefault(); 365 | 366 | var content = await response.Content.ReadAsStringAsync(); 367 | var splitContent = content.Split('|'); 368 | 369 | Assert.Equal(header, splitContent[0]); 370 | Assert.Equal(splitContent[0], splitContent[1]); 371 | 372 | // compare that second request matches the header and the scoped value 373 | var request2 = new HttpRequestMessage(); 374 | 375 | var response2 = await server.CreateClient().SendAsync(request2); 376 | 377 | var header2 = response2.Headers.GetValues(expectedHeaderName).FirstOrDefault(); 378 | 379 | var content2 = await response2.Content.ReadAsStringAsync(); 380 | var splitContent2 = content2.Split('|'); 381 | 382 | Assert.Equal(header2, splitContent2[0]); 383 | Assert.Equal(splitContent2[0], splitContent2[1]); 384 | } 385 | 386 | [Fact] 387 | public async Task CorrelationId_ReturnedCorrectlyFromTransientService() 388 | { 389 | var expectedHeaderName = new CorrelationIdOptions().RequestHeader; 390 | 391 | var builder = new WebHostBuilder() 392 | .Configure(app => 393 | { 394 | app.UseCorrelationId(); 395 | app.Run(async rd => 396 | { 397 | var transient = app.ApplicationServices.GetService(typeof(TransientClass)) as TransientClass; 398 | var scoped = app.ApplicationServices.GetService(typeof(ScopedClass)) as ScopedClass; 399 | 400 | var data = Encoding.UTF8.GetBytes(transient?.GetCorrelationFromScoped + "|" + scoped?.GetCorrelationFromScoped); 401 | 402 | await rd.Response.Body.WriteAsync(data, 0, data.Length); 403 | }); 404 | }) 405 | .ConfigureServices(sc => 406 | { 407 | sc.AddDefaultCorrelationId(); 408 | sc.TryAddTransient(); 409 | sc.TryAddScoped(); 410 | }); 411 | 412 | using var server = new TestServer(builder); 413 | 414 | // compare that first request matches the header and the scoped value 415 | var request = new HttpRequestMessage(); 416 | 417 | var response = await server.CreateClient().SendAsync(request); 418 | 419 | var header = response.Headers.GetValues(expectedHeaderName).FirstOrDefault(); 420 | 421 | var content = await response.Content.ReadAsStringAsync(); 422 | var splitContent = content.Split('|'); 423 | 424 | Assert.Equal(header, splitContent[0]); 425 | Assert.Equal(splitContent[0], splitContent[1]); 426 | 427 | // compare that second request matches the header and the scoped value 428 | var request2 = new HttpRequestMessage(); 429 | 430 | var response2 = await server.CreateClient().SendAsync(request2); 431 | 432 | var header2 = response2.Headers.GetValues(expectedHeaderName).FirstOrDefault(); 433 | 434 | var content2 = await response2.Content.ReadAsStringAsync(); 435 | var splitContent2 = content2.Split('|'); 436 | 437 | Assert.Equal(header2, splitContent2[0]); 438 | Assert.Equal(splitContent2[0], splitContent2[1]); 439 | } 440 | 441 | [Fact] 442 | public async Task CorrelationContextIncludesHeaderValue_WhichMatchesTheOriginalOptionsValue() 443 | { 444 | const string customHeader = "custom-header"; 445 | 446 | var builder = new WebHostBuilder() 447 | .Configure(app => 448 | { 449 | app.UseCorrelationId(); 450 | 451 | app.Use(async (ctx, next) => 452 | { 453 | var accessor = ctx.RequestServices.GetService(); 454 | ctx.Response.StatusCode = StatusCodes.Status200OK; 455 | await ctx.Response.WriteAsync(accessor.CorrelationContext.Header); 456 | }); 457 | }) 458 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.RequestHeader = customHeader)); 459 | 460 | using var server = new TestServer(builder); 461 | 462 | var response = await server.CreateClient().GetAsync(""); 463 | 464 | var body = await response.Content.ReadAsStringAsync(); 465 | 466 | Assert.Equal(body, customHeader); 467 | } 468 | 469 | [Fact] 470 | public async Task TraceIdentifier_IsNotUpdated_WhenUpdateTraceIdentifierIsFalse() 471 | { 472 | string originalTraceIdentifier = null; 473 | string traceIdentifier = null; 474 | 475 | const string correlationId = "123456"; 476 | 477 | var builder = new WebHostBuilder() 478 | .Configure(app => 479 | { 480 | app.Use(async (ctx, next) => 481 | { 482 | originalTraceIdentifier = ctx.TraceIdentifier; 483 | await next.Invoke(); 484 | }); 485 | 486 | app.UseCorrelationId(); 487 | 488 | app.Use((ctx, next) => 489 | { 490 | traceIdentifier = ctx.TraceIdentifier; 491 | return Task.CompletedTask; 492 | }); 493 | }) 494 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.UpdateTraceIdentifier = false)); 495 | 496 | using var server = new TestServer(builder); 497 | 498 | var request = new HttpRequestMessage(); 499 | request.Headers.Add(CorrelationIdOptions.DefaultHeader, correlationId); 500 | 501 | await server.CreateClient().SendAsync(request); 502 | 503 | Assert.Equal(originalTraceIdentifier, traceIdentifier); 504 | } 505 | 506 | [Fact] 507 | public async Task TraceIdentifier_IsNotUpdated_WhenUpdateTraceIdentifierIsTrue_ButIncomingCorrelationIdIsEmpty() 508 | { 509 | string originalTraceIdentifier = null; 510 | string traceIdentifier = null; 511 | 512 | var builder = new WebHostBuilder() 513 | .Configure(app => 514 | { 515 | app.Use(async (ctx, next) => 516 | { 517 | originalTraceIdentifier = ctx.TraceIdentifier; 518 | await next.Invoke(); 519 | }); 520 | 521 | app.UseCorrelationId(); 522 | 523 | app.Use((ctx, next) => 524 | { 525 | traceIdentifier = ctx.TraceIdentifier; 526 | return Task.CompletedTask; 527 | }); 528 | }) 529 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.UpdateTraceIdentifier = true)); 530 | 531 | using var server = new TestServer(builder); 532 | 533 | var request = new HttpRequestMessage(); 534 | request.Headers.Add(CorrelationIdOptions.DefaultHeader, ""); 535 | 536 | await server.CreateClient().SendAsync(request); 537 | 538 | Assert.NotEqual(originalTraceIdentifier, traceIdentifier); 539 | } 540 | 541 | [Fact] 542 | public async Task TraceIdentifier_IsNotUpdated_WhenUpdateTraceIdentifierIsTrue_AndGeneratedCorrelationIdIsNull() 543 | { 544 | string originalTraceIdentifier = null; 545 | string traceIdentifier = null; 546 | 547 | var builder = new WebHostBuilder() 548 | .Configure(app => 549 | { 550 | app.Use(async (ctx, next) => 551 | { 552 | originalTraceIdentifier = ctx.TraceIdentifier; 553 | await next.Invoke(); 554 | }); 555 | 556 | app.UseCorrelationId(); 557 | 558 | app.Use((ctx, next) => 559 | { 560 | traceIdentifier = ctx.TraceIdentifier; 561 | return Task.CompletedTask; 562 | }); 563 | }) 564 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => { options.UpdateTraceIdentifier = true; options.CorrelationIdGenerator = () => null; })); 565 | 566 | using var server = new TestServer(builder); 567 | 568 | await server.CreateClient().GetAsync(""); 569 | 570 | Assert.Equal(originalTraceIdentifier, traceIdentifier); 571 | } 572 | 573 | [Fact] 574 | public async Task TraceIdentifier_IsUpdated_WhenUpdateTraceIdentifierIsTrue() 575 | { 576 | string traceIdentifier = null; 577 | 578 | var expectedHeaderName = new CorrelationIdOptions().RequestHeader; 579 | const string correlationId = "123456"; 580 | 581 | var builder = new WebHostBuilder() 582 | .Configure(app => 583 | { 584 | app.UseCorrelationId(); 585 | 586 | app.Use((ctx, next) => 587 | { 588 | traceIdentifier = ctx.TraceIdentifier; 589 | return Task.CompletedTask; 590 | }); 591 | }) 592 | .ConfigureServices(sc => sc.AddDefaultCorrelationId(options => options.UpdateTraceIdentifier = true)); 593 | 594 | using var server = new TestServer(builder); 595 | 596 | var request = new HttpRequestMessage(); 597 | request.Headers.Add(expectedHeaderName, correlationId); 598 | 599 | await server.CreateClient().SendAsync(request); 600 | 601 | Assert.Equal(correlationId, traceIdentifier); 602 | } 603 | 604 | private class SingletonClass 605 | { 606 | private readonly ICorrelationContextAccessor _correlationContext; 607 | 608 | public SingletonClass(ICorrelationContextAccessor correlationContext) 609 | { 610 | _correlationContext = correlationContext; 611 | } 612 | 613 | public string GetCorrelationFromScoped => _correlationContext.CorrelationContext.CorrelationId; 614 | } 615 | 616 | private class ScopedClass 617 | { 618 | private readonly ICorrelationContextAccessor _correlationContext; 619 | 620 | public ScopedClass(ICorrelationContextAccessor correlationContext) 621 | { 622 | _correlationContext = correlationContext; 623 | } 624 | 625 | public string GetCorrelationFromScoped => _correlationContext.CorrelationContext.CorrelationId; 626 | } 627 | 628 | private class TransientClass 629 | { 630 | private readonly ICorrelationContextAccessor _correlationContext; 631 | 632 | public TransientClass(ICorrelationContextAccessor correlationContext) 633 | { 634 | _correlationContext = correlationContext; 635 | } 636 | 637 | public string GetCorrelationFromScoped => _correlationContext.CorrelationContext.CorrelationId; 638 | } 639 | 640 | private class TestCorrelationIdProvider : ICorrelationIdProvider 641 | { 642 | public const string FixedCorrelationId = "TestCorrelationId"; 643 | 644 | public string GenerateCorrelationId(HttpContext context) => FixedCorrelationId; 645 | } 646 | 647 | } 648 | } 649 | --------------------------------------------------------------------------------