├── .appveyor.yml ├── .gitattributes ├── .gitignore ├── LICENSE ├── assets └── DemoRequests.png ├── build.cake ├── build.ps1 ├── cake.config ├── readme.md └── src ├── ServiceStack.Request.Correlation.sln ├── ServiceStack.Request.Correlation.sln.DotSettings ├── ServiceStack.Request.Correlation ├── Extensions │ ├── CollectionExtensions.cs │ └── RequestExtensions.cs ├── Interfaces │ └── IIdentityGenerator.cs ├── MachineIdentity.cs ├── RequestCorrelationFeature.cs ├── RustFlakesIdentityGenerator.cs ├── ServiceGatewayFactoryBaseDecorator.cs └── ServiceStack.Request.Correlation.csproj └── test ├── DemoService ├── App.config ├── DemoExternalService.cs ├── DemoGatewayService.cs ├── DemoService.cs ├── DemoService.csproj ├── MyGatewayFactory.cs ├── Postman Samples │ └── CorrelationIdDemo.json.postman_collection └── Program.cs └── ServiceStack.Request.Correlation.Tests ├── AppHostCollection.cs ├── AppHostFixture.cs ├── Extensions ├── CollectionExtensionsTests.cs └── RequestExtensionsTests.cs ├── RequestCorrelationFeatureTests.cs ├── RustFlakesIdentityGeneratorTests.cs ├── ServiceGatewayFactoryBaseDecoratorTests.cs └── ServiceStack.Request.Correlation.Tests.csproj /.appveyor.yml: -------------------------------------------------------------------------------- 1 | #---------------------------------# 2 | # Build Script # 3 | #---------------------------------# 4 | build_script: 5 | - ps: .\build.ps1 -Target AppVeyor --settings_skipverification=true 6 | 7 | 8 | # Tests 9 | test: off 10 | 11 | #---------------------------------# 12 | # Branches to build # 13 | #---------------------------------# 14 | branches: 15 | # Whitelist 16 | only: 17 | - develop 18 | - master 19 | - /release/.*/ 20 | - /hotfix/.*/ 21 | 22 | #---------------------------------# 23 | # Build Cache # 24 | #---------------------------------# 25 | cache: 26 | - src\packages -> src\**\packages.config 27 | 28 | #---------------------------------# 29 | # environment configuration # 30 | #---------------------------------# 31 | 32 | # Build worker image (VM template) 33 | image: Visual Studio 2017 34 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | .vscode/ 28 | .idea/ 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 | artifacts/ 46 | tools/ 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 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | 113 | # MightyMoose 114 | *.mm.* 115 | AutoTest.Net/ 116 | 117 | # Web workbench (sass) 118 | .sass-cache/ 119 | 120 | # Installshield output folder 121 | [Ee]xpress/ 122 | 123 | # DocProject is a documentation generator add-in 124 | DocProject/buildhelp/ 125 | DocProject/Help/*.HxT 126 | DocProject/Help/*.HxC 127 | DocProject/Help/*.hhc 128 | DocProject/Help/*.hhk 129 | DocProject/Help/*.hhp 130 | DocProject/Help/Html2 131 | DocProject/Help/html 132 | 133 | # Click-Once directory 134 | publish/ 135 | 136 | # Publish Web Output 137 | *.[Pp]ublish.xml 138 | *.azurePubxml 139 | ## TODO: Comment the next line if you want to checkin your 140 | ## web deploy settings but do note that will include unencrypted 141 | ## passwords 142 | #*.pubxml 143 | 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Windows Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Windows Store app package directory 160 | AppPackages/ 161 | 162 | # Visual Studio cache files 163 | # files ending in .cache can be ignored 164 | *.[Cc]ache 165 | # but keep track of directories ending in .cache 166 | !*.[Cc]ache/ 167 | 168 | # Others 169 | ClientBin/ 170 | [Ss]tyle[Cc]op.* 171 | ~$* 172 | *~ 173 | *.dbmdl 174 | *.dbproj.schemaview 175 | *.pfx 176 | *.publishsettings 177 | node_modules/ 178 | orleans.codegen.cs 179 | 180 | # RIA/Silverlight projects 181 | Generated_Code/ 182 | 183 | # Backup & report files from converting an old project file 184 | # to a newer Visual Studio version. Backup files are not needed, 185 | # because we have git ;-) 186 | _UpgradeReport_Files/ 187 | Backup*/ 188 | UpgradeLog*.XML 189 | UpgradeLog*.htm 190 | 191 | # SQL Server files 192 | *.mdf 193 | *.ldf 194 | 195 | # Business Intelligence projects 196 | *.rdl.data 197 | *.bim.layout 198 | *.bim_*.settings 199 | 200 | # Microsoft Fakes 201 | FakesAssemblies/ 202 | 203 | # Node.js Tools for Visual Studio 204 | .ntvs_analysis.dat 205 | 206 | # Visual Studio 6 build log 207 | *.plg 208 | 209 | # Visual Studio 6 workspace options file 210 | *.opt 211 | 212 | # LightSwitch generated files 213 | GeneratedArtifacts/ 214 | _Pvt_Extensions/ 215 | ModelManifest.xml 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwlicious/servicestack-request-correlation/3d4e032384513f412b02a6c980bd269c1e5239d8/LICENSE -------------------------------------------------------------------------------- /assets/DemoRequests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwwlicious/servicestack-request-correlation/3d4e032384513f412b02a6c980bd269c1e5239d8/assets/DemoRequests.png -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | #tool "nuget:?package=xunit.runner.console&version=2.3.1" 2 | #tool "nuget:?package=GitVersion.CommandLine&version=3.6.5" 3 | #tool "nuget:?package=gitreleasemanager&version=0.7.1" 4 | #tool "nuget:?package=gitlink&version=2.4.0" 5 | #addin "nuget:?package=Cake.Incubator&version=3.0.0" 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // ARGUMENTS 9 | /////////////////////////////////////////////////////////////////////////////// 10 | var envBuildNumber = EnvironmentVariable("APPVEYOR_BUILD_NUMBER", 0); 11 | var gitHubUserName = EnvironmentVariable("GITHUB_USERNAME"); 12 | var gitHubPassword = EnvironmentVariable("GITHUB_PASSWORD"); 13 | var nugetSourceUrl = EnvironmentVariable("NUGET_SOURCE"); 14 | var nugetApiKey = EnvironmentVariable("NUGET_API_KEY"); 15 | 16 | var target = Argument("target", "Default"); 17 | var configuration = Argument("configuration", "Release"); 18 | var buildNumber = Argument("buildNumber", envBuildNumber); 19 | 20 | /////////////////////////////////////////////////////////////////////////////// 21 | // VARIABLES 22 | /////////////////////////////////////////////////////////////////////////////// 23 | 24 | // folders 25 | var artifactsDir = Directory("./artifacts"); 26 | var nugetPackageDir = artifactsDir + Directory("nuget-packages"); 27 | var srcDir = Directory("./src"); 28 | var rootPath = MakeAbsolute(Directory("./")); 29 | var releaseNotesPath = rootPath.CombineWithFilePath("CHANGELOG.md"); 30 | 31 | // project specific 32 | var solutionFile = srcDir + File("servicestack.request.correlation.sln"); 33 | var gitHubRepositoryOwner = "wwwlicious"; 34 | var gitHubRepositoryName = "servicestack-request-correlation"; 35 | 36 | var isLocalBuild = BuildSystem.IsLocalBuild; 37 | var isPullRequest = BuildSystem.AppVeyor.Environment.PullRequest.IsPullRequest; 38 | var isMasterBranch = BuildSystem.AppVeyor.Environment.Repository.Branch.EqualsIgnoreCase("master"); 39 | var isReleaseBranch = BuildSystem.AppVeyor.Environment.Repository.Branch.StartsWithIgnoreCase("release"); 40 | var isHotFixBranch = BuildSystem.AppVeyor.Environment.Repository.Branch.StartsWithIgnoreCase("hotfix"); 41 | var isTagged = BuildSystem.AppVeyor.Environment.Repository.Tag.IsTag && !BuildSystem.AppVeyor.Environment.Repository.Tag.Name.IsNullOrEmpty(); 42 | var publishingError = false; 43 | 44 | var shouldPublishNuGet = (!isLocalBuild && !isPullRequest && (isMasterBranch || isReleaseBranch || isHotFixBranch) && isTagged); 45 | var shouldPublishGitHub = shouldPublishNuGet; 46 | 47 | var gitVersionResults = GitVersion(new GitVersionSettings { UpdateAssemblyInfo = false }); 48 | var semVersion = $"{gitVersionResults.MajorMinorPatch}.{buildNumber}"; 49 | 50 | Information("SemverVersion -> {0}", semVersion); 51 | 52 | var projects = ParseSolution(solutionFile).GetProjects().Select(x => ParseProject(x.Path, configuration)); 53 | 54 | /////////////////////////////////////////////////////////////////////////////// 55 | // SETUP / TEARDOWN 56 | /////////////////////////////////////////////////////////////////////////////// 57 | 58 | Setup(ctx => 59 | { 60 | // Executed BEFORE the first task. 61 | Information("Running tasks..."); 62 | 63 | if(isMasterBranch && (ctx.Log.Verbosity != Verbosity.Diagnostic)) { 64 | Information("Increasing verbosity to diagnostic."); 65 | ctx.Log.Verbosity = Verbosity.Diagnostic; 66 | } 67 | }); 68 | 69 | Teardown(ctx => 70 | { 71 | // Executed AFTER the last task. 72 | Information("Finished running tasks."); 73 | }); 74 | 75 | /////////////////////////////////////////////////////////////////////////////// 76 | // TASKS 77 | /////////////////////////////////////////////////////////////////////////////// 78 | 79 | Task("Default") 80 | .IsDependentOn("Clean") 81 | .IsDependentOn("Build") 82 | .IsDependentOn("Test"); 83 | 84 | Task("Build") 85 | .Does(() => { 86 | Information("Building {0}", solutionFile); 87 | var msbuildBinaryLogFile = artifactsDir + new FilePath(solutionFile.Path.GetFilenameWithoutExtension() + ".binlog"); 88 | 89 | MSBuild(solutionFile.Path, settings => { 90 | settings 91 | .SetConfiguration(configuration) 92 | .SetMaxCpuCount(0) // use as many cpu's as are available 93 | .WithRestore() 94 | .WithProperty("TreatresultsAsErrors", "false") 95 | .WithProperty("resultsAsErrors", "3884") 96 | .WithProperty("CodeContractsRunCodeAnalysis", "true") 97 | .WithProperty("RunCodeAnalysis", "false") 98 | .WithProperty("Version", semVersion) 99 | .WithProperty("PackageVersion", gitVersionResults.MajorMinorPatch) 100 | .WithProperty("PackageOutputPath", MakeAbsolute(nugetPackageDir).FullPath) 101 | .UseToolVersion(MSBuildToolVersion.VS2017) 102 | .SetNodeReuse(false); 103 | 104 | // setup binary logging for solution to artifacts dir 105 | settings.ArgumentCustomization = arguments => { 106 | arguments.Append(string.Format("/bl:{0}", msbuildBinaryLogFile)); 107 | return arguments; 108 | }; 109 | }); 110 | }); 111 | 112 | Task("Test") 113 | .Does(() => { 114 | Information("Testing for {0}", solutionFile); 115 | var testProjects = projects.Where(x => x.IsTestProject()); 116 | foreach(var proj in testProjects){ 117 | DotNetCoreTest(proj.ProjectFilePath.FullPath); 118 | } 119 | }); 120 | 121 | Task("ReleaseNotes") 122 | .IsDependentOn("Create-Release-Notes"); 123 | 124 | Task("AppVeyor") 125 | .IsDependentOn("Default") 126 | .IsDependentOn("Upload-AppVeyor-Artifacts") 127 | .IsDependentOn("Publish-Nuget-Packages") 128 | .IsDependentOn("Publish-GitHub-Release") 129 | .Finally(() => 130 | { 131 | if(publishingError) 132 | { 133 | throw new Exception($"An error occurred during the publishing of {solutionFile.Path}. All publishing tasks have been attempted."); 134 | } 135 | }); 136 | 137 | Task("Create-Release-Notes") 138 | .Does(() => { 139 | Information("Creating release notes for {0}", semVersion); 140 | gitHubUserName.ThrowIfNull(nameof(gitHubUserName)); 141 | gitHubPassword.ThrowIfNull(nameof(gitHubPassword)); 142 | GitReleaseManagerCreate(gitHubUserName, gitHubPassword, gitHubRepositoryOwner, gitHubRepositoryName, new GitReleaseManagerCreateSettings { 143 | Milestone = gitVersionResults.MajorMinorPatch, 144 | Name = gitVersionResults.MajorMinorPatch, 145 | Prerelease = false, 146 | TargetCommitish = "master", 147 | }); 148 | }); 149 | 150 | Task("Export-Release-Notes") 151 | .WithCriteria(() => !isLocalBuild) 152 | .WithCriteria(() => BuildSystem.IsRunningOnAppVeyor && !isPullRequest) 153 | .WithCriteria(() => isMasterBranch || isReleaseBranch || isHotFixBranch) 154 | .WithCriteria(() => isTagged) 155 | .Does(() => { 156 | Information("Exporting release notes for {0}", solutionFile); 157 | gitHubUserName.ThrowIfNull(nameof(gitHubUserName)); 158 | gitHubPassword.ThrowIfNull(nameof(gitHubPassword)); 159 | 160 | GitReleaseManagerExport(gitHubUserName, gitHubPassword, gitHubRepositoryOwner, gitHubRepositoryName, releaseNotesPath, 161 | new GitReleaseManagerExportSettings { 162 | TagName = gitVersionResults.MajorMinorPatch 163 | }); 164 | }); 165 | 166 | Task("Publish-GitHub-Release") 167 | .IsDependentOn("Export-Release-Notes") 168 | .WithCriteria(() => shouldPublishGitHub) 169 | .Does(() => { 170 | Information("Publishing github release for {0}", solutionFile); 171 | gitHubUserName.ThrowIfNull(nameof(gitHubUserName)); 172 | gitHubPassword.ThrowIfNull(nameof(gitHubPassword)); 173 | 174 | // upload packages as assets 175 | foreach(var package in GetFiles(nugetPackageDir.Path + "/*")) 176 | { 177 | GitReleaseManagerAddAssets(gitHubUserName, gitHubPassword, gitHubRepositoryOwner, gitHubRepositoryName, gitVersionResults.MajorMinorPatch, package.ToString()); 178 | } 179 | 180 | // close the release 181 | GitReleaseManagerClose(gitHubUserName, gitHubPassword, gitHubRepositoryOwner, gitHubRepositoryName, gitVersionResults.MajorMinorPatch); 182 | }); 183 | 184 | Task("Publish-Nuget-Packages") 185 | .WithCriteria(() => shouldPublishNuGet) 186 | .WithCriteria(() => DirectoryExists(nugetPackageDir)) 187 | .Does(() => { 188 | 189 | Information("Publishing NuGet Packages for {0}", solutionFile); 190 | 191 | nugetSourceUrl.ThrowIfNull(nameof(nugetSourceUrl)); 192 | nugetApiKey.ThrowIfNull(nameof(nugetApiKey)); 193 | var nupkgFiles = GetFiles(nugetPackageDir.Path + "/**/*.nupkg"); 194 | 195 | foreach(var nupkgFile in nupkgFiles) 196 | { 197 | // Push the package. 198 | NuGetPush(nupkgFile, new NuGetPushSettings { 199 | Source = nugetSourceUrl, 200 | ApiKey = nugetApiKey, 201 | 202 | }); 203 | } 204 | }); 205 | 206 | 207 | Task("Upload-AppVeyor-Artifacts") 208 | .IsDependentOn("Export-Release-Notes") 209 | .WithCriteria(() => BuildSystem.IsRunningOnAppVeyor) 210 | .WithCriteria(() => DirectoryExists(nugetPackageDir)) 211 | .Does(() => { 212 | Information("Uploading AppVeyor artifacts for {0}", solutionFile); 213 | foreach(var package in GetFiles(nugetPackageDir.Path + "/*")) 214 | { 215 | AppVeyor.UploadArtifact(package); 216 | } 217 | }); 218 | 219 | Task("Sample") 220 | .Does(() => { 221 | Information("Restoring NuGet Packages for {0}", solutionFile); 222 | }); 223 | 224 | Task("Clean") 225 | .Does(() => { 226 | CleanDirectories(new DirectoryPath[] { 227 | artifactsDir, 228 | nugetPackageDir 229 | }); 230 | }); 231 | 232 | RunTarget(target); -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER ShowDescription 25 | Shows description about tasks. 26 | .PARAMETER DryRun 27 | Performs a dry run. 28 | .PARAMETER Experimental 29 | Uses the nightly builds of the Roslyn script engine. 30 | .PARAMETER Mono 31 | Uses the Mono Compiler rather than the Roslyn script engine. 32 | .PARAMETER SkipToolPackageRestore 33 | Skips restoring of packages. 34 | .PARAMETER ScriptArgs 35 | Remaining arguments are added here. 36 | 37 | .LINK 38 | https://cakebuild.net 39 | 40 | #> 41 | 42 | [CmdletBinding()] 43 | Param( 44 | [string]$Script = "build.cake", 45 | [string]$Target, 46 | [string]$Configuration, 47 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 48 | [string]$Verbosity, 49 | [switch]$ShowDescription, 50 | [Alias("WhatIf", "Noop")] 51 | [switch]$DryRun, 52 | [switch]$Experimental, 53 | [switch]$Mono, 54 | [switch]$SkipToolPackageRestore, 55 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 56 | [string[]]$ScriptArgs 57 | ) 58 | 59 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 60 | function MD5HashFile([string] $filePath) 61 | { 62 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 63 | { 64 | return $null 65 | } 66 | 67 | [System.IO.Stream] $file = $null; 68 | [System.Security.Cryptography.MD5] $md5 = $null; 69 | try 70 | { 71 | $md5 = [System.Security.Cryptography.MD5]::Create() 72 | $file = [System.IO.File]::OpenRead($filePath) 73 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 74 | } 75 | finally 76 | { 77 | if ($file -ne $null) 78 | { 79 | $file.Dispose() 80 | } 81 | } 82 | } 83 | 84 | function GetProxyEnabledWebClient 85 | { 86 | $wc = New-Object System.Net.WebClient 87 | $proxy = [System.Net.WebRequest]::GetSystemWebProxy() 88 | $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials 89 | $wc.Proxy = $proxy 90 | return $wc 91 | } 92 | 93 | Write-Host "Preparing to run build script..." 94 | 95 | if(!$PSScriptRoot){ 96 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 97 | } 98 | 99 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 100 | $ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" 101 | $MODULES_DIR = Join-Path $TOOLS_DIR "Modules" 102 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 103 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 104 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 105 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 106 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 107 | $ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" 108 | $MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" 109 | 110 | # Make sure tools folder exists 111 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 112 | Write-Verbose -Message "Creating tools directory..." 113 | New-Item -Path $TOOLS_DIR -Type directory | out-null 114 | } 115 | 116 | # Make sure that packages.config exist. 117 | if (!(Test-Path $PACKAGES_CONFIG)) { 118 | Write-Verbose -Message "Downloading packages.config..." 119 | try { 120 | $wc = GetProxyEnabledWebClient 121 | $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) 122 | } catch { 123 | Throw "Could not download packages.config." 124 | } 125 | } 126 | 127 | # Try find NuGet.exe in path if not exists 128 | if (!(Test-Path $NUGET_EXE)) { 129 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 130 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } 131 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 132 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 133 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 134 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 135 | } 136 | } 137 | 138 | # Try download NuGet.exe if not exists 139 | if (!(Test-Path $NUGET_EXE)) { 140 | Write-Verbose -Message "Downloading NuGet.exe..." 141 | try { 142 | $wc = GetProxyEnabledWebClient 143 | $wc.DownloadFile($NUGET_URL, $NUGET_EXE) 144 | } catch { 145 | Throw "Could not download NuGet.exe." 146 | } 147 | } 148 | 149 | # Save nuget.exe path to environment to be available to child processed 150 | $ENV:NUGET_EXE = $NUGET_EXE 151 | 152 | # Restore tools from NuGet? 153 | if(-Not $SkipToolPackageRestore.IsPresent) { 154 | Push-Location 155 | Set-Location $TOOLS_DIR 156 | 157 | # Check for changes in packages.config and remove installed tools if true. 158 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 159 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 160 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 161 | Write-Verbose -Message "Missing or changed package.config hash..." 162 | Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | 163 | Remove-Item -Recurse 164 | } 165 | 166 | Write-Verbose -Message "Restoring tools from NuGet..." 167 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 168 | 169 | if ($LASTEXITCODE -ne 0) { 170 | Throw "An error occurred while restoring NuGet tools." 171 | } 172 | else 173 | { 174 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 175 | } 176 | Write-Verbose -Message ($NuGetOutput | out-string) 177 | 178 | Pop-Location 179 | } 180 | 181 | # Restore addins from NuGet 182 | if (Test-Path $ADDINS_PACKAGES_CONFIG) { 183 | Push-Location 184 | Set-Location $ADDINS_DIR 185 | 186 | Write-Verbose -Message "Restoring addins from NuGet..." 187 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" 188 | 189 | if ($LASTEXITCODE -ne 0) { 190 | Throw "An error occurred while restoring NuGet addins." 191 | } 192 | 193 | Write-Verbose -Message ($NuGetOutput | out-string) 194 | 195 | Pop-Location 196 | } 197 | 198 | # Restore modules from NuGet 199 | if (Test-Path $MODULES_PACKAGES_CONFIG) { 200 | Push-Location 201 | Set-Location $MODULES_DIR 202 | 203 | Write-Verbose -Message "Restoring modules from NuGet..." 204 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" 205 | 206 | if ($LASTEXITCODE -ne 0) { 207 | Throw "An error occurred while restoring NuGet modules." 208 | } 209 | 210 | Write-Verbose -Message ($NuGetOutput | out-string) 211 | 212 | Pop-Location 213 | } 214 | 215 | # Make sure that Cake has been installed. 216 | if (!(Test-Path $CAKE_EXE)) { 217 | Throw "Could not find Cake.exe at $CAKE_EXE" 218 | } 219 | 220 | 221 | 222 | # Build Cake arguments 223 | $cakeArguments = @("$Script"); 224 | if ($Target) { $cakeArguments += "-target=$Target" } 225 | if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } 226 | if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } 227 | if ($ShowDescription) { $cakeArguments += "-showdescription" } 228 | if ($DryRun) { $cakeArguments += "-dryrun" } 229 | if ($Experimental) { $cakeArguments += "-experimental" } 230 | if ($Mono) { $cakeArguments += "-mono" } 231 | $cakeArguments += $ScriptArgs 232 | 233 | # Start Cake 234 | Write-Host "Running build script..." 235 | &$CAKE_EXE $cakeArguments 236 | exit $LASTEXITCODE 237 | -------------------------------------------------------------------------------- /cake.config: -------------------------------------------------------------------------------- 1 | ; This is the default configuration file for Cake. 2 | ; This file was downloaded from https://github.com/cake-build/resources 3 | 4 | [Nuget] 5 | Source=https://api.nuget.org/v3/index.json 6 | UseInProcessClient=true 7 | LoadDependencies=false 8 | 9 | [Paths] 10 | Tools=./tools 11 | Addins=./tools/Addins 12 | Modules=./tools/Modules 13 | 14 | [Settings] 15 | SkipVerification=false 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ServiceStack.Request.Correlation 2 | [![Build status](https://ci.appveyor.com/api/projects/status/h3mps66bduii6amq/branch/master?svg=true)](https://ci.appveyor.com/project/wwwlicious/servicestack-request-correlation/branch/master) 3 | [![NuGet version](https://badge.fury.io/nu/ServiceStack.Request.Correlation.svg)](https://badge.fury.io/nu/ServiceStack.Request.Correlation) 4 | 5 | A plugin for [ServiceStack](https://servicestack.net/) that creates a correlation id that allows requests to be tracked across multiple services. 6 | 7 | If no correlation id is found one is created and appended to incoming `IRequest` object, both as a header and in the `IRequest.Items` collection. The correlation id is then added to outgoing `IResponse` object. 8 | 9 | If a correlation id already exists then this is appended to the outgoing response object. 10 | 11 | ## Quick Start 12 | 13 | Install the package [https://www.nuget.org/packages/ServiceStack.Request.Correlation](https://www.nuget.org/packages/ServiceStack.Request.Correlation/) 14 | ```bash 15 | PM> Install-Package ServiceStack.Request.Correlation 16 | ``` 17 | 18 | The plugin is added like any other. By default it has no external dependencies that need to be provided. 19 | ```csharp 20 | Plugins.Add(new RequestCorrelationFeature()); 21 | ``` 22 | 23 | This will ensure an http header `x-mac-requestid` is added to requests with a unique id in every request/response. 24 | 25 | ### Customising 26 | Both the http header name (default: "x-mac-requestid") and request id generation method (default: [RustFlakes](https://github.com/peschkaj/rustflakes)) can be customised: 27 | ```csharp 28 | Plugins.Add(new RequestCorrelationFeature 29 | { 30 | HeaderName = "x-my-custom-header", 31 | IdentityGenerator = new MyIdentityGenerator() 32 | }); 33 | 34 | // public class MyIdentityGenerator : IIdentityGenerator { ... } 35 | ``` 36 | Where `IIdentityGenerator` has a single method that needs to be implemented. This method is called each time a request id is created: 37 | ```csharp 38 | string GenerateIdentity(); 39 | ``` 40 | 41 | ## Demo 42 | There is a demo project, DemoService, within the solution which will show some of the concepts. This is a console app that starts a self-hosted app host on http://127.0.0.1:8090 with the request correlation plugin setup using "x-my-requestId" as header name and a simple incrementing identity generator, and a sample ServiceGatewayFactory registered in DI for making external requests. 43 | 44 | The "Postman Samples" folder contains a sample [Postman](https://www.getpostman.com/) collection containing test calls. Use the "Import" function in Postman to import this collection. 45 | 46 | Sample calls are made to DemoGatewayService. If `{"internal":true}` then an inprocess request is made. If `{"internal":false}` then an external call is made (albeit to the same host). In both instances the correlation id travels to the downstream services (shown in header and body). 47 | 48 | ![Demo Requests](assets/DemoRequests.png) 49 | 50 | ## Why? 51 | When used with alongside a logging strategy it will enable tracing of requests across multiple services. 52 | 53 | When working with distributed systems it becomes very valuable to track which services/systems an originating request has touched. 54 | 55 | ## How? 56 | The plugin registers a [`PreRequestFilter`](https://github.com/ServiceStack/ServiceStack/wiki/Order-of-Operations) which checks the `IRequest` object for the existance of a correlation Id and creates one if there isn't one. 57 | 58 | A [`GlobalResponseFilter`](https://github.com/ServiceStack/ServiceStack/wiki/Order-of-Operations) is used to append the correlation id to any outgoing `IResponse` objects. 59 | 60 | ### Internal and External calls 61 | To support persisting the correlation id across external calls, on `AfterPluginsLoaded` the plugin checks to see if there is an `IServiceGatewayFactory` registered in the IoC container. If there is, and it is also an `ServiceGatewayFactoryBase`, then it registers a decorator, `ServiceGatewayFactoryBaseDecorator`, to add the correlation id to any outgoing external requests made via anything implementing `IRestClient`, (e.g. `JsonServiceClient` or `JsvServiceClient`). 62 | 63 | If the call is internal then the same `IRequest` object will be used so the correlation id will already be present. 64 | 65 | ## Limitations 66 | 67 | ### Identity Generation 68 | The default `IIdentityGenerator` implementation uses [RustFlakes](https://github.com/peschkaj/rustflakes) for identity generation. This creates time-based sequential ids that can be generated from multiple sources without risking a collision. 69 | 70 | The correlation id stays the same across multiple service requests. One point that I would like to improve on is having a 'base' correlation id that can be augmented as it travels to different services to allow a [vector clock](https://en.wikipedia.org/wiki/Vector_clock), or similar, to be built. 71 | 72 | ### External Calls 73 | Appending the correlation id to external calls is currently done via a decorator on registered `ServiceGatewayFactoryBase` but this feels a little clunky. I'll endeavour to have a cleaner method of ensuring the correlation id is appended to external, as well as internal, requests without having a dependency on certain items being registered within the IoC container. 74 | -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.24720.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack.Request.Correlation", "ServiceStack.Request.Correlation\ServiceStack.Request.Correlation.csproj", "{68FDD37A-B15E-4EE3-9F9D-652D35F66524}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1B21B7CF-AA63-44FD-98C1-CBAB86F21D7A}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{16BD1096-59FD-420E-896B-048D6BEDAAB9}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack.Request.Correlation.Tests", "test\ServiceStack.Request.Correlation.Tests\ServiceStack.Request.Correlation.Tests.csproj", "{DF483953-95AA-4849-B500-A5A995C89130}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoService", "test\DemoService\DemoService.csproj", "{F7792FC8-9CF9-488B-864D-865D55C2BBB1}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {68FDD37A-B15E-4EE3-9F9D-652D35F66524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {68FDD37A-B15E-4EE3-9F9D-652D35F66524}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {68FDD37A-B15E-4EE3-9F9D-652D35F66524}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {68FDD37A-B15E-4EE3-9F9D-652D35F66524}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {DF483953-95AA-4849-B500-A5A995C89130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {DF483953-95AA-4849-B500-A5A995C89130}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {DF483953-95AA-4849-B500-A5A995C89130}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {DF483953-95AA-4849-B500-A5A995C89130}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {F7792FC8-9CF9-488B-864D-865D55C2BBB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {F7792FC8-9CF9-488B-864D-865D55C2BBB1}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {F7792FC8-9CF9-488B-864D-865D55C2BBB1}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {F7792FC8-9CF9-488B-864D-865D55C2BBB1}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(NestedProjects) = preSolution 39 | {16BD1096-59FD-420E-896B-048D6BEDAAB9} = {1B21B7CF-AA63-44FD-98C1-CBAB86F21D7A} 40 | {DF483953-95AA-4849-B500-A5A995C89130} = {1B21B7CF-AA63-44FD-98C1-CBAB86F21D7A} 41 | {F7792FC8-9CF9-488B-864D-865D55C2BBB1} = {16BD1096-59FD-420E-896B-048D6BEDAAB9} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Extensions 5 | { 6 | using System.Collections.Generic; 7 | 8 | public static class CollectionExtensions 9 | { 10 | public static void InsertAsFirst(this List list, T item) 11 | { 12 | list?.Insert(0, item); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/Extensions/RequestExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Extensions 5 | { 6 | using Web; 7 | 8 | public static class RequestExtensions 9 | { 10 | public static string GetCorrelationId(this IRequest request, string headerName) 11 | { 12 | var correlationId = request.Headers[headerName]; 13 | if (string.IsNullOrWhiteSpace(correlationId)) 14 | { 15 | return request.Items.TryGetValue(headerName, out var correlationObj) ? correlationObj.ToString() : null; 16 | } 17 | 18 | return correlationId; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/Interfaces/IIdentityGenerator.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Interfaces 5 | { 6 | /// 7 | /// Contains method used to generate request correlation id 8 | /// 9 | public interface IIdentityGenerator 10 | { 11 | /// 12 | /// Generate a string that will uniquely identify a request 13 | /// 14 | /// 15 | string GenerateIdentity(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/MachineIdentity.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation 5 | { 6 | using System; 7 | using System.Net.NetworkInformation; 8 | using System.Threading.Tasks; 9 | using Logging; 10 | 11 | // TODO Extract this to an interface to allow it to be switched out. 12 | // TODO Create Docker implementation. Use hash of Docker ContainerName + ImageName to get unique. Add on startup as outside concern. 13 | public class MachineIdentity 14 | { 15 | private static readonly ILog log; 16 | 17 | static MachineIdentity() 18 | { 19 | log = LogManager.GetLogger(typeof(MachineIdentity)); 20 | } 21 | 22 | // Give 1 second to generate the identifier 23 | private const int TimeOut = 1000; 24 | 25 | /// 26 | /// Gets a unique machine identifier from the machines MAC address. 27 | /// 28 | /// 29 | public static uint GetMachineIdentifier() 30 | { 31 | // Get the first MAC address and use that as machine identifier. 32 | try 33 | { 34 | var task = Task.Factory.StartNew(GetMacAddressBasedIdentifier); 35 | 36 | if (!task.Wait(TimeOut)) 37 | { 38 | throw new TimeoutException("Timeout exceeded generating machine identifier"); 39 | } 40 | 41 | // No MAC addresses, return current timestamp. 42 | return task.Result ?? Convert.ToUInt32(DateTime.Now.TimeOfDay.TotalMilliseconds); 43 | } 44 | catch (AggregateException ae) 45 | { 46 | foreach (var e in ae.Flatten().InnerExceptions) 47 | { 48 | log.Error("Error getting machine identifier", e); 49 | } 50 | 51 | throw; 52 | } 53 | catch (Exception ex) 54 | { 55 | log.Error("Error getting machine identifier", ex); 56 | throw; 57 | } 58 | } 59 | 60 | private static uint? GetMacAddressBasedIdentifier() 61 | { 62 | foreach (var ni in NetworkInterface.GetAllNetworkInterfaces()) 63 | { 64 | // discard because of standard reasons 65 | if (IsLoopBackOrTunnel(ni)) 66 | continue; 67 | 68 | // discard virtual cards (virtual box, virtual pc, etc.) 69 | if (IsVirtualCard(ni)) 70 | continue; 71 | 72 | if (IsMicrosoftLoopback(ni)) 73 | continue; 74 | 75 | var address = ni.GetPhysicalAddress(); 76 | var bytes = address.GetAddressBytes(); 77 | 78 | if (bytes.Length > 0) 79 | { 80 | var uniqueId = BitConverter.ToUInt32(bytes, 0); 81 | return uniqueId; 82 | } 83 | } 84 | 85 | log.Info("Failed to get a unique identifier from mac address"); 86 | return null; 87 | } 88 | 89 | private static bool IsLoopBackOrTunnel(NetworkInterface ni) 90 | { 91 | return (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) || 92 | (ni.NetworkInterfaceType == NetworkInterfaceType.Tunnel); 93 | } 94 | 95 | private static bool IsVirtualCard(NetworkInterface ni) 96 | { 97 | 98 | const string @virtual = "virtual"; 99 | return (ni.Description.IndexOf(@virtual, StringComparison.OrdinalIgnoreCase) >= 0) || 100 | (ni.Name.IndexOf(@virtual, StringComparison.OrdinalIgnoreCase) >= 0); 101 | } 102 | 103 | private static bool IsMicrosoftLoopback(NetworkInterface ni) 104 | { 105 | // "Microsoft Loopback Adapter" will not show as NetworkInterfaceType.Loopback 106 | const string loopbackAdapter = "Microsoft Loopback Adapter"; 107 | return ni.Description.Equals(loopbackAdapter, StringComparison.OrdinalIgnoreCase); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/RequestCorrelationFeature.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation 5 | { 6 | using Extensions; 7 | using Interfaces; 8 | using Logging; 9 | using ServiceStack; 10 | using Web; 11 | 12 | public class RequestCorrelationFeature : IPlugin, IPostInitPlugin 13 | { 14 | public string HeaderName { get; set; } = "x-mac-requestId"; 15 | 16 | public IIdentityGenerator IdentityGenerator { get; set; } = new RustFlakesIdentityGenerator(); 17 | 18 | private readonly ILog log = LogManager.GetLogger(typeof(RequestCorrelationFeature)); 19 | 20 | public void Register(IAppHost appHost) 21 | { 22 | appHost.PreRequestFilters.InsertAsFirst(ProcessRequest); 23 | 24 | appHost.GlobalResponseFilters.Add(SetResponseCorrelationId); 25 | } 26 | 27 | public virtual void ProcessRequest(IRequest request, IResponse response) 28 | { 29 | // Check for existence of header. If not there add it in 30 | var correlationId = request.GetCorrelationId(HeaderName); 31 | log.Debug($"Got correlation Id {correlationId ?? "" } with key {HeaderName} from incoming request object"); 32 | if (string.IsNullOrWhiteSpace(correlationId)) 33 | { 34 | correlationId = IdentityGenerator.GenerateIdentity(); 35 | request.Headers[HeaderName] = correlationId; 36 | log.Debug($"Generated new correlation Id {correlationId} for key {HeaderName} on incoming request object"); 37 | } 38 | 39 | request.Items[HeaderName] = correlationId; 40 | } 41 | 42 | public virtual void SetResponseCorrelationId(IRequest request, IResponse response, object dto) 43 | { 44 | var correlationId = request.GetCorrelationId(HeaderName); 45 | log.Debug($"Setting correlation Id {correlationId} to header {HeaderName} on response object"); 46 | 47 | response.AddHeader(HeaderName, correlationId); 48 | } 49 | 50 | public void AfterPluginsLoaded(IAppHost appHost) 51 | { 52 | // Check if an IServiceGatewayFactory has been registered 53 | var factory = appHost.TryResolve(); 54 | 55 | if (factory is ServiceGatewayFactoryBase factoryBase) 56 | appHost.Register(new ServiceGatewayFactoryBaseDecorator(HeaderName, factoryBase)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/RustFlakesIdentityGenerator.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation 5 | { 6 | using System.Globalization; 7 | using Interfaces; 8 | using RustFlakes; 9 | 10 | public class RustFlakesIdentityGenerator : IIdentityGenerator 11 | { 12 | // NOTE DecimalOxidation is not the fastest nor smallest but happy medium 13 | private readonly DecimalOxidation generator; 14 | 15 | public RustFlakesIdentityGenerator() 16 | { 17 | var machineIdentifier = MachineIdentity.GetMachineIdentifier(); 18 | generator = new DecimalOxidation(machineIdentifier); 19 | } 20 | 21 | public string GenerateIdentity() 22 | { 23 | return generator.Oxidize().ToString(CultureInfo.InvariantCulture); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/ServiceGatewayFactoryBaseDecorator.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation 5 | { 6 | using System; 7 | using Extensions; 8 | using Web; 9 | 10 | public class ServiceGatewayFactoryBaseDecorator : ServiceGatewayFactoryBase 11 | { 12 | private readonly string headerName; 13 | private readonly ServiceGatewayFactoryBase gatewayFactory; 14 | private string correlationId; 15 | 16 | public ServiceGatewayFactoryBaseDecorator(string headerName, ServiceGatewayFactoryBase factory) 17 | { 18 | headerName.ThrowIfNullOrEmpty(nameof(headerName)); 19 | factory.ThrowIfNull(nameof(factory)); 20 | 21 | this.headerName = headerName; 22 | gatewayFactory = factory; 23 | } 24 | 25 | public override IServiceGateway GetServiceGateway(IRequest request) 26 | { 27 | correlationId = request.GetCorrelationId(headerName); 28 | 29 | // This call needs to be made to ensure the internal localGateway is setup 30 | gatewayFactory.GetServiceGateway(request); 31 | return this; 32 | } 33 | 34 | public override IServiceGateway GetGateway(Type requestType) 35 | { 36 | var serviceGateway = gatewayFactory.GetGateway(requestType); 37 | 38 | if (string.IsNullOrEmpty(correlationId)) 39 | { 40 | return serviceGateway; 41 | } 42 | 43 | if (!(serviceGateway is IRestClient restClient)) 44 | { 45 | // Internal call, no need to do anything as using same request/response objects 46 | return serviceGateway; 47 | } 48 | 49 | // External call, add this to the headers collection to be added to outgoing request object 50 | restClient.AddHeader(headerName, correlationId); 51 | return serviceGateway; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/ServiceStack.Request.Correlation/ServiceStack.Request.Correlation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net462 4 | Debug;Release 5 | Plugin for ServiceStack that ensures there's a unique request correlation Id in header 6 | ServiceStack.Request.Correlation 7 | $(version) 8 | Donald Gray (@donaldgray);Scott Mackay (@wwwlicious) 9 | https://github.com/wwwlicious/servicestack-request-correlation 10 | https://opensource.org/licenses/MPL-2.0 11 | https://servicestack.net/img/logo-32.png 12 | https://github.com/wwwlicious/servicestack-request-correlation 13 | servicestack request correlation microservices 14 | See github project for details 15 | true 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/DemoService/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/DemoService/DemoExternalService.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace DemoService 5 | { 6 | using ServiceStack; 7 | using ServiceStack.Text; 8 | 9 | public class DemoExternalService : Service 10 | { 11 | public object Any(DemoExternalRequest demoRequest) 12 | { 13 | var header = Request.Headers[HeaderNames.CorrelationId]; 14 | 15 | $"Demo service received external request with {header} value for {HeaderNames.CorrelationId} header".Print(); 16 | 17 | return new DemoResponse { Message = $"External request id = {header}" }; 18 | } 19 | } 20 | 21 | public class DemoExternalRequest : IReturn 22 | { 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/DemoService/DemoGatewayService.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace DemoService 5 | { 6 | using ServiceStack; 7 | 8 | public class DemoGatewayService : Service 9 | { 10 | public object Any(DemoGatewayRequest demoRequest) 11 | { 12 | if (demoRequest.Internal) 13 | return Gateway.Send(demoRequest.ConvertTo()); 14 | 15 | return Gateway.Send(demoRequest.ConvertTo()); 16 | } 17 | } 18 | 19 | public class DemoGatewayRequest : IReturn 20 | { 21 | public bool Internal { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/DemoService/DemoService.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace DemoService 5 | { 6 | using ServiceStack; 7 | using ServiceStack.Text; 8 | 9 | public class DemoService : Service 10 | { 11 | public object Any(DemoRequest demoRequest) 12 | { 13 | var header = Request.Headers[HeaderNames.CorrelationId]; 14 | 15 | // Request.Items[HeaderNames.CorrelationId] 16 | 17 | $"Demo service received request with {header} value for {HeaderNames.CorrelationId} header".Print(); 18 | 19 | return new DemoResponse { Message = $"Internal request id = {header}" }; 20 | } 21 | } 22 | 23 | public class DemoRequest : IReturn 24 | { 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/DemoService/DemoService.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net462 4 | Debug;Release 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/DemoService/MyGatewayFactory.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace DemoService 5 | { 6 | using System; 7 | using ServiceStack; 8 | 9 | public class MyGatewayFactory : ServiceGatewayFactoryBase 10 | { 11 | // from https://forums.servicestack.net/t/servicestack-discovery-consul-rfc/2042/21 12 | public override IServiceGateway GetGateway(Type requestType) 13 | { 14 | // If dto contains "External" then make an external request to it, else inProc 15 | var gateway = requestType.Name.Contains("External") 16 | ? new JsonServiceClient("http://127.0.0.1:8090/") 17 | : (IServiceGateway)localGateway; 18 | return gateway; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/test/DemoService/Postman Samples/CorrelationIdDemo.json.postman_collection: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a5e6c9e5-3fba-bbde-ec1c-bd42d3e4c4cf", 3 | "name": "CorrelationId Demo", 4 | "description": "", 5 | "order": [ 6 | "719a4da7-d3d5-e589-dd2e-37eddf874066", 7 | "fcf0a67d-7e5d-a21a-a17f-3b61e191adc3", 8 | "c1919e55-56b5-ca09-da92-80935eb2e226", 9 | "beab586d-1243-d494-0311-f595898e3f13" 10 | ], 11 | "folders": [], 12 | "timestamp": 1461149045872, 13 | "owner": "", 14 | "remoteLink": "", 15 | "public": false, 16 | "requests": [ 17 | { 18 | "id": "719a4da7-d3d5-e589-dd2e-37eddf874066", 19 | "headers": "Accept: application/json\nContent-Type: application/json\n", 20 | "url": "http://127.0.0.1:8090/json/reply/demogatewayrequest", 21 | "pathVariables": {}, 22 | "preRequestScript": null, 23 | "method": "POST", 24 | "collectionId": "a5e6c9e5-3fba-bbde-ec1c-bd42d3e4c4cf", 25 | "data": [], 26 | "dataMode": "raw", 27 | "name": "CorrelationId internal", 28 | "description": "Request to gateway for forwarded inproc request", 29 | "descriptionFormat": "html", 30 | "time": 1461149045871, 31 | "version": 2, 32 | "responses": [], 33 | "tests": null, 34 | "currentHelper": "normal", 35 | "helperAttributes": {}, 36 | "rawModeData": "{ \"Internal\": true }" 37 | }, 38 | { 39 | "id": "beab586d-1243-d494-0311-f595898e3f13", 40 | "headers": "Accept: application/json\nContent-Type: application/json\nx-my-requestId: demoValue\n", 41 | "url": "http://127.0.0.1:8090/json/reply/demogatewayrequest", 42 | "pathVariables": {}, 43 | "preRequestScript": null, 44 | "method": "POST", 45 | "collectionId": "a5e6c9e5-3fba-bbde-ec1c-bd42d3e4c4cf", 46 | "data": [], 47 | "dataMode": "raw", 48 | "name": "CorrelationId external with correlationId", 49 | "description": "Request to gateway for forwarded external request, with already existing correlation id", 50 | "descriptionFormat": "html", 51 | "time": 1461150612337, 52 | "version": 2, 53 | "responses": [], 54 | "tests": null, 55 | "currentHelper": "normal", 56 | "helperAttributes": {}, 57 | "rawModeData": "{ \"Internal\": false }" 58 | }, 59 | { 60 | "id": "c1919e55-56b5-ca09-da92-80935eb2e226", 61 | "headers": "Accept: application/json\nContent-Type: application/json\nx-my-requestId: demoValue\n", 62 | "url": "http://127.0.0.1:8090/json/reply/demogatewayrequest", 63 | "pathVariables": {}, 64 | "preRequestScript": null, 65 | "method": "POST", 66 | "collectionId": "a5e6c9e5-3fba-bbde-ec1c-bd42d3e4c4cf", 67 | "data": [], 68 | "dataMode": "raw", 69 | "name": "CorrelationId internal with correlationId", 70 | "description": "Request to gateway for forwarded inproc request, with already existing correlation id", 71 | "descriptionFormat": "html", 72 | "time": 1461150580559, 73 | "version": 2, 74 | "responses": [], 75 | "tests": null, 76 | "currentHelper": "normal", 77 | "helperAttributes": {}, 78 | "rawModeData": "{ \"Internal\": true }" 79 | }, 80 | { 81 | "id": "fcf0a67d-7e5d-a21a-a17f-3b61e191adc3", 82 | "headers": "Accept: application/json\nContent-Type: application/json\n", 83 | "url": "http://127.0.0.1:8090/json/reply/demogatewayrequest", 84 | "pathVariables": {}, 85 | "preRequestScript": null, 86 | "method": "POST", 87 | "collectionId": "a5e6c9e5-3fba-bbde-ec1c-bd42d3e4c4cf", 88 | "data": [], 89 | "dataMode": "raw", 90 | "name": "CorrelationId external", 91 | "description": "Request to gateway for forwarded external request", 92 | "descriptionFormat": "html", 93 | "time": 1461149078019, 94 | "version": 2, 95 | "responses": [], 96 | "tests": null, 97 | "currentHelper": "normal", 98 | "helperAttributes": {}, 99 | "rawModeData": "{ \"Internal\": false }" 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /src/test/DemoService/Program.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace DemoService 5 | { 6 | using System; 7 | using System.Diagnostics; 8 | using Funq; 9 | using ServiceStack; 10 | using ServiceStack.Logging; 11 | using ServiceStack.Request.Correlation; 12 | using ServiceStack.Request.Correlation.Interfaces; 13 | using ServiceStack.Text; 14 | using ServiceStack.Web; 15 | 16 | class Program 17 | { 18 | static void Main(string[] args) 19 | { 20 | var serviceUrl = "http://127.0.0.1:8090/"; 21 | new AppHost(serviceUrl).Init().Start("http://*:8090/"); 22 | $"ServiceStack SelfHost listening at {serviceUrl} ".Print(); 23 | Process.Start(serviceUrl); 24 | 25 | Console.ReadLine(); 26 | } 27 | } 28 | 29 | public class AppHost : AppSelfHostBase 30 | { 31 | private readonly string serviceUrl; 32 | 33 | public AppHost(string serviceUrl) : base("DemoService", typeof (DemoService).Assembly) 34 | { 35 | this.serviceUrl = serviceUrl; 36 | } 37 | 38 | public override void Configure(Container container) 39 | { 40 | SetConfig(new HostConfig 41 | { 42 | WebHostUrl = serviceUrl, 43 | ApiVersion = "2.0" 44 | }); 45 | 46 | //LogManager.LogFactory = new ConsoleLogFactory(); 47 | 48 | // Register a ServiceGatewayFactory for making 'external' calls 49 | Container.RegisterFactory(() => new MyGatewayFactory()).ReusedWithin(ReuseScope.None); 50 | 51 | // Default plugin with 'x-mac-requestId' headername and Rustflakes generator 52 | // Plugins.Add(new RequestCorrelationFeature()); 53 | 54 | // Customised plugin 55 | Plugins.Add(new RequestCorrelationFeature 56 | { 57 | HeaderName = HeaderNames.CorrelationId, 58 | IdentityGenerator = new IncrementingIdentityGenerator() 59 | }); 60 | } 61 | } 62 | 63 | public class DemoResponse 64 | { 65 | public string Message { get; set; } 66 | } 67 | 68 | public static class HeaderNames 69 | { 70 | public const string CorrelationId = "x-my-requestId"; 71 | } 72 | 73 | public class DateTimeIdentityGenerator : IIdentityGenerator 74 | { 75 | public string GenerateIdentity() 76 | { 77 | return DateTime.Now.ToString("O"); 78 | } 79 | } 80 | 81 | public class IncrementingIdentityGenerator : IIdentityGenerator 82 | { 83 | private int count; 84 | public string GenerateIdentity() 85 | { 86 | return (++count).ToString(); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/AppHostCollection.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace ServiceStack.Configuration.Consul.Tests.Fixtures 6 | { 7 | using Xunit; 8 | 9 | [CollectionDefinition("AppHost")] 10 | public class AppHostCollection : ICollectionFixture 11 | { 12 | // marker class for tests that require apphost.init 13 | // http://xunit.github.io/docs/shared-context.html 14 | } 15 | } -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/AppHostFixture.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | namespace ServiceStack.Configuration.Consul.Tests.Fixtures 6 | { 7 | using System; 8 | using Testing; 9 | 10 | public class AppHostFixture : IDisposable 11 | { 12 | public BasicAppHost AppHost { get; set; } 13 | 14 | public const string WebHostUrl = "http://127.0.0.1:8090"; 15 | public const string HandlerFactoryPath = "api"; 16 | public const string ServiceName = "testService"; 17 | 18 | public AppHostFixture() 19 | { 20 | var hostConfig = new HostConfig 21 | { 22 | WebHostUrl = WebHostUrl, 23 | HandlerFactoryPath = HandlerFactoryPath, 24 | DebugMode = true 25 | }; 26 | 27 | AppHost = new BasicAppHost 28 | { 29 | TestMode = true, 30 | Config = hostConfig, 31 | ServiceName = ServiceName 32 | }; 33 | 34 | AppHost.Init(); 35 | AppHost.Config.WebHostUrl = WebHostUrl; 36 | AppHost.Config.HandlerFactoryPath = HandlerFactoryPath; 37 | } 38 | 39 | public void Dispose() => AppHost?.Dispose(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/Extensions/CollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Tests.Extensions 5 | { 6 | using System.Collections.Generic; 7 | using Correlation.Extensions; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | public class CollectionExtensionsTests 12 | { 13 | [Fact] 14 | public void InsertAsFirst_HandlesNullList() 15 | { 16 | List list = null; 17 | 18 | list.InsertAsFirst("hi"); 19 | 20 | // No assertion, no error thrown 21 | } 22 | 23 | [Fact] 24 | public void InsertAsFirst_AddsAtPosition0_EmptyList() 25 | { 26 | var list = new List(); 27 | 28 | const string toAdd = "hi"; 29 | list.InsertAsFirst(toAdd); 30 | 31 | list[0].Should().Be(toAdd); 32 | } 33 | 34 | [Fact] 35 | public void InsertAsFirst_AddsAtPosition0_PopulatedList() 36 | { 37 | var list = new List { "One", "Two", "Three" }; 38 | 39 | const string toAdd = "hi"; 40 | list.InsertAsFirst(toAdd); 41 | 42 | list[0].Should().Be(toAdd); 43 | } 44 | 45 | [Fact] 46 | public void InsertAsFirst_AddToPopulatedList() 47 | { 48 | var list = new List { "One", "Two", "Three" }; 49 | 50 | list.InsertAsFirst("hi"); 51 | 52 | list.Count.Should().Be(4); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/Extensions/RequestExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Tests.Extensions 5 | { 6 | using System; 7 | using Correlation.Extensions; 8 | using FluentAssertions; 9 | using ServiceStack.Configuration.Consul.Tests.Fixtures; 10 | using Testing; 11 | using Xunit; 12 | 13 | [Collection("AppHost")] 14 | public class RequestExtensionsTests 15 | { 16 | private ServiceStackHost appHost; 17 | 18 | private const string headerName = "x-correlationId"; 19 | private readonly string headerValue = Guid.NewGuid().ToString(); 20 | private readonly string itemValue = Guid.NewGuid().ToString(); 21 | 22 | public RequestExtensionsTests(AppHostFixture fixture) 23 | { 24 | appHost = fixture.AppHost; 25 | } 26 | 27 | [Fact] 28 | public void GetCorrelationId_ReturnsNull_IfNotSet() 29 | { 30 | var request = new MockHttpRequest(); 31 | 32 | request.GetCorrelationId(headerName).Should().BeNull(); 33 | } 34 | 35 | [Fact] 36 | public void GetCorrelationId_ReturnsHeaderValue_IfPresent() 37 | { 38 | var request = new MockHttpRequest(); 39 | request.Headers.Add(headerName, headerValue); 40 | 41 | var correlationId = request.GetCorrelationId(headerName); 42 | 43 | correlationId.Should().Be(headerValue); 44 | } 45 | 46 | [Fact] 47 | public void GetCorrelationId_ReturnsHeaderValue_IfBothHeaderAndItemsSet() 48 | { 49 | var request = new MockHttpRequest(); 50 | request.Headers.Add(headerName, headerValue); 51 | request.Items.Add(headerName, itemValue); 52 | 53 | var correlationId = request.GetCorrelationId(headerName); 54 | 55 | correlationId.Should().Be(headerValue); 56 | } 57 | 58 | [Fact] 59 | public void GetCorrelationId_ReturnsItemValue_IfNotSet() 60 | { 61 | var request = new MockHttpRequest(); 62 | request.Items.Add(headerName, itemValue); 63 | 64 | var correlationId = request.GetCorrelationId(headerName); 65 | 66 | correlationId.Should().Be(itemValue); 67 | } 68 | 69 | [Theory] 70 | [InlineData(null)] 71 | [InlineData("")] 72 | [InlineData(" ")] 73 | public void GetCorrelationId_ReturnsItemValue_HeaderNullOrEmpty(string header) 74 | { 75 | var request = new MockHttpRequest(); 76 | request.Items.Add(headerName, itemValue); 77 | request.Headers.Add(headerName, header); 78 | 79 | var correlationId = request.GetCorrelationId(headerName); 80 | 81 | correlationId.Should().Be(itemValue); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/RequestCorrelationFeatureTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Tests 5 | { 6 | using System; 7 | using FakeItEasy; 8 | using FluentAssertions; 9 | using Interfaces; 10 | using ServiceStack; 11 | using ServiceStack.Configuration.Consul.Tests.Fixtures; 12 | using Testing; 13 | using Web; 14 | using Xunit; 15 | 16 | [Collection("AppHost")] 17 | public class RequestCorrelationFeatureTests 18 | { 19 | private readonly RequestCorrelationFeature feature; 20 | private readonly IIdentityGenerator generator; 21 | private readonly string newId = Guid.NewGuid().ToString(); 22 | private readonly ServiceStackHost appHost; 23 | 24 | public RequestCorrelationFeatureTests(AppHostFixture fixture) 25 | { 26 | appHost = fixture.AppHost; 27 | generator = A.Fake(); 28 | A.CallTo(() => generator.GenerateIdentity()).Returns(newId); 29 | feature = new RequestCorrelationFeature { IdentityGenerator = generator }; 30 | } 31 | 32 | [Fact] 33 | public void Register_AddsPreRequestFilter() 34 | { 35 | appHost.PreRequestFilters.Count.Should().Be(0); 36 | 37 | feature.Register(appHost); 38 | 39 | appHost.PreRequestFilters.Count.Should().Be(1); 40 | } 41 | 42 | [Fact] 43 | public void Register_AddsPreRequestFilter_AtPosition0() 44 | { 45 | Action myDelegate = (request, response) => { }; 46 | 47 | // Add delegate at position 0 48 | appHost.PreRequestFilters.Insert(0, myDelegate); 49 | 50 | feature.Register(appHost); 51 | 52 | // After registering delegate added at 0 should now be at 1 53 | appHost.PreRequestFilters[1].Should().Be(myDelegate); 54 | } 55 | 56 | [Fact] 57 | public void Register_AddsResponseFilter() 58 | { 59 | var filterCount = appHost.GlobalResponseFilters.Count; 60 | 61 | feature.Register(appHost); 62 | 63 | appHost.GlobalResponseFilters.Count.Should().Be(filterCount + 1); 64 | } 65 | 66 | [Fact] 67 | public void HeaderName_HasDefaultValue() 68 | { 69 | var testFeature = new RequestCorrelationFeature(); 70 | testFeature.HeaderName.Should().NotBeNullOrWhiteSpace(); 71 | } 72 | 73 | [Fact] 74 | public void IdentityGenerator_UsesRustFlakesIdentityGeneratorByDefault() 75 | { 76 | var testFeature = new RequestCorrelationFeature(); 77 | testFeature.IdentityGenerator.Should().BeOfType(); 78 | } 79 | 80 | [Fact] 81 | public void ProcessRequest_SetsHeaderOnRequest_IfNotProvided() 82 | { 83 | var mockHttpRequest = new MockHttpRequest(); 84 | 85 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 86 | 87 | mockHttpRequest.Headers[feature.HeaderName].Should().NotBeNullOrEmpty(); 88 | } 89 | 90 | [Fact] 91 | public void ProcessRequest_UsesCustomHeaderName_IfSet() 92 | { 93 | var mockHttpRequest = new MockHttpRequest(); 94 | 95 | const string customHeader = "x-my-test-header"; 96 | feature.HeaderName = customHeader; 97 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 98 | 99 | mockHttpRequest.Headers[customHeader].Should().NotBeNullOrEmpty(); 100 | } 101 | 102 | [Fact] 103 | public void ProcessRequest_SetsNewIdOnRequestHeader_IfNotProvided() 104 | { 105 | var mockHttpRequest = new MockHttpRequest(); 106 | 107 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 108 | 109 | mockHttpRequest.Headers[feature.HeaderName].Should().Be(newId); 110 | } 111 | 112 | [Fact] 113 | public void ProcessRequest_AddsNewIdOnRequestItems_IfNotProvided() 114 | { 115 | var mockHttpRequest = new MockHttpRequest(); 116 | 117 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 118 | 119 | mockHttpRequest.Items[feature.HeaderName].ToString().Should().Be(newId); 120 | } 121 | 122 | [Theory] 123 | [InlineData(null)] 124 | [InlineData("")] 125 | [InlineData(" ")] 126 | public void ProcessRequest_SetsNewIdOnRequestHeader_IfProvidedButEmpty(string requestId) 127 | { 128 | var mockHttpRequest = new MockHttpRequest(); 129 | mockHttpRequest.Headers[feature.HeaderName] = requestId; 130 | 131 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 132 | 133 | mockHttpRequest.Headers[feature.HeaderName].Should().Be(newId); 134 | } 135 | 136 | [Theory] 137 | [InlineData(null)] 138 | [InlineData("")] 139 | [InlineData(" ")] 140 | public void ProcessRequest_SetsNewIdOnRequestItems_IfProvidedButEmpty(string requestId) 141 | { 142 | var mockHttpRequest = new MockHttpRequest(); 143 | mockHttpRequest.Headers[feature.HeaderName] = requestId; 144 | 145 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 146 | 147 | mockHttpRequest.Items[feature.HeaderName].Should().Be(newId); 148 | } 149 | 150 | [Fact] 151 | public void ProcessRequest_DoesNotChangeHeaderOnRequest_IfProvided() 152 | { 153 | var requestId = Guid.NewGuid().ToString(); 154 | var mockHttpRequest = new MockHttpRequest(); 155 | mockHttpRequest.Headers[feature.HeaderName] = requestId; 156 | 157 | feature.ProcessRequest(mockHttpRequest, new MockHttpResponse()); 158 | 159 | mockHttpRequest.Headers[feature.HeaderName].Should().Be(requestId); 160 | } 161 | 162 | [Fact] 163 | public void SetResponseCorrelationId_SetsIdOnResponse_IfInRequest() 164 | { 165 | var requestId = Guid.NewGuid().ToString(); 166 | var mockHttpResponse = new MockHttpResponse(); 167 | var mockHttpRequest = new MockHttpRequest(); 168 | mockHttpRequest.Headers[feature.HeaderName] = requestId; 169 | 170 | feature.SetResponseCorrelationId(mockHttpRequest, mockHttpResponse, 1); 171 | 172 | mockHttpResponse.Headers[feature.HeaderName].Should().Be(requestId); 173 | } 174 | 175 | [Fact] 176 | public void AfterPluginsLoaded_GetsIServiceGatewayFactory_FromContainer() 177 | { 178 | var appHost = A.Fake(); 179 | 180 | feature.AfterPluginsLoaded(appHost); 181 | 182 | A.CallTo(() => appHost.TryResolve()).MustHaveHappened(); 183 | } 184 | 185 | [Fact] 186 | public void AfterPluginsLoaded_RegistersDecorator_IfIServiceGatewayFactory_IsServiceGatewayFactoryBase() 187 | { 188 | var appHost = A.Fake(); 189 | A.CallTo(() => appHost.TryResolve()).Returns(new TestServiceGatewayFactory()); 190 | 191 | feature.AfterPluginsLoaded(appHost); 192 | 193 | A.CallTo(() => 194 | appHost.Register( 195 | A.That.Matches(g => g.GetType() == typeof(ServiceGatewayFactoryBaseDecorator)))) 196 | .MustHaveHappened(); 197 | } 198 | 199 | [Fact] 200 | public void AfterPluginsLoaded_DoesNotRegistersDecorator_IfIServiceGatewayFactory_IsNotServiceGatewayFactoryBase() 201 | { 202 | var appHost = A.Fake(); 203 | A.CallTo(() => appHost.TryResolve()).Returns(new BasicServiceGatewayFactory()); 204 | 205 | feature.AfterPluginsLoaded(appHost); 206 | 207 | A.CallTo(() => appHost.Register(A.Ignored)) 208 | .MustNotHaveHappened(); 209 | } 210 | } 211 | 212 | public class TestServiceGatewayFactory : ServiceGatewayFactoryBase 213 | { 214 | public override IServiceGateway GetGateway(Type requestType) 215 | { 216 | throw new NotImplementedException(); 217 | } 218 | } 219 | 220 | public class BasicServiceGatewayFactory : IServiceGatewayFactory 221 | { 222 | public IServiceGateway GetServiceGateway(IRequest request) 223 | { 224 | throw new NotImplementedException(); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/RustFlakesIdentityGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Tests 5 | { 6 | using System.Linq; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | public class RustFlakesIdentityGeneratorTests 11 | { 12 | private readonly RustFlakesIdentityGenerator generator; 13 | 14 | public RustFlakesIdentityGeneratorTests() 15 | { 16 | generator = new RustFlakesIdentityGenerator(); 17 | } 18 | 19 | [Fact] 20 | public void GenerateIdentity_GeneratesString() 21 | { 22 | var str = generator.GenerateIdentity(); 23 | str.Should().NotBeNullOrWhiteSpace(); 24 | } 25 | 26 | [Fact] 27 | public void GenerateIdentity_GeneratesUnique() 28 | { 29 | var collection = Enumerable.Range(0, 100).Select(_ => generator.GenerateIdentity()).ToArray(); 30 | 31 | collection.Length.Should().Be(collection.Distinct().Count()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/ServiceGatewayFactoryBaseDecoratorTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | namespace ServiceStack.Request.Correlation.Tests 5 | { 6 | using System; 7 | using FakeItEasy; 8 | using FluentAssertions; 9 | using ServiceStack.Configuration.Consul.Tests.Fixtures; 10 | using Testing; 11 | using Xunit; 12 | 13 | [Collection("AppHost")] 14 | public class ServiceGatewayFactoryBaseDecoratorTests 15 | { 16 | private readonly ServiceStackHost appHost; 17 | private readonly ServiceGatewayFactoryBaseDecorator gateway; 18 | private readonly ServiceGatewayFactoryBase decorated; 19 | private const string HeaderName = "x-correlation-id"; 20 | private readonly string correlationId = Guid.NewGuid().ToString(); 21 | 22 | public ServiceGatewayFactoryBaseDecoratorTests(AppHostFixture fixture) 23 | { 24 | appHost = fixture.AppHost; 25 | decorated = A.Fake(); 26 | gateway = new ServiceGatewayFactoryBaseDecorator(HeaderName, decorated); 27 | } 28 | 29 | [Theory] 30 | [InlineData(null)] 31 | [InlineData("")] 32 | public void Ctor_Throws_IfHeaderNameNullOrEmpty(string header) 33 | { 34 | Assert.Throws(() => new ServiceGatewayFactoryBaseDecorator(header, decorated)); 35 | } 36 | 37 | [Fact] 38 | public void Ctor_Throws_IfFactoryBaseNull() 39 | { 40 | Assert.Throws(() => new ServiceGatewayFactoryBaseDecorator(HeaderName, null)); 41 | } 42 | 43 | [Fact] 44 | public void GetServiceGateway_CallsUnderlyingGetServiceGateway() 45 | { 46 | var mockHttpRequest = new MockHttpRequest(); 47 | gateway.GetServiceGateway(mockHttpRequest); 48 | 49 | A.CallTo(() => decorated.GetServiceGateway(mockHttpRequest)).MustHaveHappened(); 50 | } 51 | 52 | [Fact] 53 | public void GetServiceGateway_ReturnsSelf() 54 | { 55 | var result = gateway.GetServiceGateway(new MockHttpRequest()); 56 | result.Should().Be(gateway); 57 | } 58 | 59 | [Fact] 60 | public void GetGateway_CallsUnderlyingGetGateway() 61 | { 62 | var type = typeof (int); 63 | gateway.GetGateway(type); 64 | 65 | A.CallTo(() => decorated.GetGateway(type)).MustHaveHappened(); 66 | } 67 | 68 | [Fact] 69 | public void GetGateway_ReturnsUnderlyingGetGateway_IfNoCorrelationId() 70 | { 71 | var type = typeof(int); 72 | var serviceGateway = A.Fake(); 73 | A.CallTo(() => decorated.GetGateway(type)).Returns(serviceGateway); 74 | 75 | gateway.GetGateway(type).Should().Be(serviceGateway); 76 | } 77 | 78 | [Fact] 79 | public void GetGateway_Returns_UnderlyingGetGateway_IfCorrelationIdAndNotServiceClientBase() 80 | { 81 | SetCorrelationId(); 82 | var type = typeof(int); 83 | var serviceGateway = A.Fake(); 84 | A.CallTo(() => decorated.GetGateway(type)).Returns(serviceGateway); 85 | 86 | gateway.GetGateway(type).Should().Be(serviceGateway); 87 | } 88 | 89 | [Fact] 90 | public void GetGateway_SetsUnderlyingServiceClientBaseHeaders_IfCorrelationIdAndServiceClientBase() 91 | { 92 | SetCorrelationId(); 93 | var type = typeof(int); 94 | 95 | // The code checks for IRestClient but IServiceClient implements this, and also implemetns IServiceGateway 96 | var client = A.Fake(); 97 | A.CallTo(() => decorated.GetGateway(type)).Returns(client); 98 | 99 | gateway.GetGateway(type); 100 | 101 | A.CallTo(() => client.AddHeader(HeaderName, correlationId)).MustHaveHappened(); 102 | } 103 | 104 | private void SetCorrelationId() 105 | { 106 | var mock = new MockHttpRequest(); 107 | mock.Headers[HeaderName] = correlationId; 108 | 109 | gateway.GetServiceGateway(mock); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/ServiceStack.Request.Correlation.Tests/ServiceStack.Request.Correlation.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.0 4 | Debug;Release 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------