diff --git a/samples/MvcCorrelationIdSample/Controllers/ValuesController.cs b/samples/MvcCorrelationIdSample/Controllers/ValuesController.cs index 8c790f9..ab8c12a 100644 --- a/samples/MvcCorrelationIdSample/Controllers/ValuesController.cs +++ b/samples/MvcCorrelationIdSample/Controllers/ValuesController.cs @@ -32,7 +32,8 @@ public IEnumerable Get() $"DirectAccessor={correlation}", $"Transient={_transient.GetCorrelationFromScoped}", $"Scoped={_scoped.GetCorrelationFromScoped}", - $"Singleton={_singleton.GetCorrelationFromScoped}" + $"Singleton={_singleton.GetCorrelationFromScoped}", + $"TraceIdentifier={HttpContext.TraceIdentifier}" }; } } diff --git a/samples/MvcCorrelationIdSample/Properties/launchSettings.json b/samples/MvcCorrelationIdSample/Properties/launchSettings.json index f159df5..095496b 100644 --- a/samples/MvcCorrelationIdSample/Properties/launchSettings.json +++ b/samples/MvcCorrelationIdSample/Properties/launchSettings.json @@ -9,21 +9,21 @@ }, "profiles": { "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "api/values", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "http://localhost:59922/api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, "MvcCorrelationIdSample": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "api/values", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:59923/" + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:59923/api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:59923/" } } } diff --git a/src/CorrelationId/CorrelationContext.cs b/src/CorrelationId/CorrelationContext.cs index dab91fd..5e18821 100644 --- a/src/CorrelationId/CorrelationContext.cs +++ b/src/CorrelationId/CorrelationContext.cs @@ -7,17 +7,26 @@ namespace CorrelationId /// public class CorrelationContext { - internal CorrelationContext(string correlationId) + internal CorrelationContext(string correlationId, string header) { if (string.IsNullOrEmpty(correlationId)) throw new ArgumentNullException(nameof(correlationId)); + if (string.IsNullOrEmpty(header)) + throw new ArgumentNullException(nameof(header)); + CorrelationId = correlationId; + Header = header; } /// /// The Correlation ID which is applicable to the current request. /// public string CorrelationId { get; } + + /// + /// The name of the header from which the Correlation ID is read/written. + /// + public string Header { get; } } } diff --git a/src/CorrelationId/CorrelationContextAccessor.cs b/src/CorrelationId/CorrelationContextAccessor.cs index 4a79c69..3ef0241 100644 --- a/src/CorrelationId/CorrelationContextAccessor.cs +++ b/src/CorrelationId/CorrelationContextAccessor.cs @@ -7,6 +7,7 @@ public class CorrelationContextAccessor : ICorrelationContextAccessor { private static AsyncLocal _correlationContext = new AsyncLocal(); + /// public CorrelationContext CorrelationContext { get => _correlationContext.Value; diff --git a/src/CorrelationId/CorrelationContextFactory.cs b/src/CorrelationId/CorrelationContextFactory.cs index 7e9cb9e..cda90fa 100644 --- a/src/CorrelationId/CorrelationContextFactory.cs +++ b/src/CorrelationId/CorrelationContextFactory.cs @@ -4,16 +4,27 @@ public class CorrelationContextFactory : ICorrelationContextFactory { private readonly ICorrelationContextAccessor _correlationContextAccessor; - + + /// + /// Initializes a new instance of the class. + /// + public CorrelationContextFactory() + : this(null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The through which the will be set. public CorrelationContextFactory(ICorrelationContextAccessor correlationContextAccessor) { _correlationContextAccessor = correlationContextAccessor; } /// - public CorrelationContext Create(string correlationId) + public CorrelationContext Create(string correlationId, string header) { - var correlationContext = new CorrelationContext(correlationId); + var correlationContext = new CorrelationContext(correlationId, header); if (_correlationContextAccessor != null) { diff --git a/src/CorrelationId/CorrelationId.csproj b/src/CorrelationId/CorrelationId.csproj index 9f27662..8bfa477 100644 --- a/src/CorrelationId/CorrelationId.csproj +++ b/src/CorrelationId/CorrelationId.csproj @@ -11,12 +11,20 @@ git aspnetcore;correlationid en - 2.0.1 + 2.1.0 bin\Release\netstandard1.4\CorrelationId.xml https://github.com/stevejgordon/CorrelationId https://github.com/stevejgordon/CorrelationId + + latest + + + + latest + + diff --git a/src/CorrelationId/CorrelationIdMiddleware.cs b/src/CorrelationId/CorrelationIdMiddleware.cs index 004cfc7..e20a4e7 100644 --- a/src/CorrelationId/CorrelationIdMiddleware.cs +++ b/src/CorrelationId/CorrelationIdMiddleware.cs @@ -2,13 +2,18 @@ using Microsoft.Extensions.Options; using System; using System.Threading.Tasks; +using Microsoft.Extensions.Primitives; namespace CorrelationId { + /// + /// Middleware which attempts to reads / creates a Correlation ID that can then be used in logs and + /// passed to upstream requests. + /// public class CorrelationIdMiddleware { private readonly RequestDelegate _next; - private readonly IOptions _options; + private readonly CorrelationIdOptions _options; /// /// Creates a new instance of the CorrelationIdMiddleware. @@ -18,7 +23,7 @@ public class CorrelationIdMiddleware public CorrelationIdMiddleware(RequestDelegate next, IOptions options) { _next = next ?? throw new ArgumentNullException(nameof(next)); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options.Value ?? throw new ArgumentNullException(nameof(options)); } /// @@ -27,26 +32,25 @@ public CorrelationIdMiddleware(RequestDelegate next, IOptions /// The for the current request. /// The which can create a . - /// public async Task Invoke(HttpContext context, ICorrelationContextFactory correlationContextFactory) { - if (context.Request.Headers.TryGetValue(_options.Value.Header, out var correlationId)) - { + var correlationId = SetCorrelationId(context); + + if (_options.UpdateTraceIdentifier) context.TraceIdentifier = correlationId; - } - correlationContextFactory.Create(context.TraceIdentifier); + correlationContextFactory.Create(correlationId, _options.Header); - if (_options.Value.IncludeInResponse) + if (_options.IncludeInResponse) { // apply the correlation ID to the response header for client side tracking context.Response.OnStarting(() => { - if (!context.Response.Headers.ContainsKey(_options.Value.Header)) + if (!context.Response.Headers.ContainsKey(_options.Header)) { - context.Response.Headers.Add(_options.Value.Header, context.TraceIdentifier); + context.Response.Headers.Add(_options.Header, correlationId); } - + return Task.CompletedTask; }); } @@ -55,5 +59,21 @@ public async Task Invoke(HttpContext context, ICorrelationContextFactory correla correlationContextFactory.Dispose(); } + + private StringValues SetCorrelationId(HttpContext context) + { + var correlationIdFoundInRequestHeader = context.Request.Headers.TryGetValue(_options.Header, out var correlationId); + + if (RequiresGenerationOfCorrelationId(correlationIdFoundInRequestHeader, correlationId)) + correlationId = GenerateCorrelationId(context.TraceIdentifier); + + return correlationId; + } + + private static bool RequiresGenerationOfCorrelationId(bool idInHeader, StringValues idFromHeader) => + !idInHeader || StringValues.IsNullOrEmpty(idFromHeader); + + private StringValues GenerateCorrelationId(string traceIdentifier) => + _options.UseGuidForCorrelationId || string.IsNullOrEmpty(traceIdentifier) ? Guid.NewGuid().ToString() : traceIdentifier; } } diff --git a/src/CorrelationId/CorrelationIdOptions.cs b/src/CorrelationId/CorrelationIdOptions.cs index de0aec8..dced528 100644 --- a/src/CorrelationId/CorrelationIdOptions.cs +++ b/src/CorrelationId/CorrelationIdOptions.cs @@ -8,13 +8,33 @@ public class CorrelationIdOptions private const string DefaultHeader = "X-Correlation-ID"; /// - /// The header field name where the correlation ID will be stored. + /// The name of the header from which the Correlation ID is read/written. /// public string Header { get; set; } = DefaultHeader; /// + /// /// Controls whether the correlation ID is returned in the response headers. + /// + /// Default: true /// public bool IncludeInResponse { get; set; } = true; + + /// + /// + /// Controls whether the ASP.NET Core TraceIdentifier will be set to match the CorrelationId. + /// + /// Default: true + /// + public bool UpdateTraceIdentifier { get; set; } = true; + + /// + /// + /// Controls whether a GUID will be used in cases where no correlation ID is retrieved from the request header. + /// When false the TraceIdentifier for the current request will be used. + /// + /// Default: false. + /// + public bool UseGuidForCorrelationId { get; set; } = false; } } diff --git a/src/CorrelationId/CorrelationIdServiceExtensions.cs b/src/CorrelationId/CorrelationIdServiceExtensions.cs index 58be269..de98d86 100644 --- a/src/CorrelationId/CorrelationIdServiceExtensions.cs +++ b/src/CorrelationId/CorrelationIdServiceExtensions.cs @@ -3,6 +3,9 @@ namespace CorrelationId { + /// + /// Extensions on the . + /// public static class CorrelationIdServiceExtensions { /// diff --git a/src/CorrelationId/ICorrelationContextFactory.cs b/src/CorrelationId/ICorrelationContextFactory.cs index 582ae3a..1008d49 100644 --- a/src/CorrelationId/ICorrelationContextFactory.cs +++ b/src/CorrelationId/ICorrelationContextFactory.cs @@ -9,8 +9,9 @@ public interface ICorrelationContextFactory /// Creates a new with the correlation ID set for the current request. /// /// The correlation ID to set on the context. + /// /// The header used to hold the correlation ID. /// A new instance of a . - CorrelationContext Create(string correlationId); + CorrelationContext Create(string correlationId, string header); /// /// Disposes of the for the current request. diff --git a/test/CorrelationId.Tests/CorrelationIdMiddlewareTests.cs b/test/CorrelationId.Tests/CorrelationIdMiddlewareTests.cs index 7e4717a..a1a0fab 100644 --- a/test/CorrelationId.Tests/CorrelationIdMiddlewareTests.cs +++ b/test/CorrelationId.Tests/CorrelationIdMiddlewareTests.cs @@ -6,8 +6,11 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; +using System; namespace CorrelationId.Tests { @@ -50,7 +53,7 @@ public async Task DoesNotThrowException_WhenOptionSetToTrue_IfHeaderIsAlreadySet } [Fact] - public async Task DoesNotReturnCorrelationIdInResponseHeader_WhenOptionSetToFalse() + public async Task DoesNotReturnCorrelationIdInResponseHeader_WhenIncludeInResponseIsFalse() { var options = new CorrelationIdOptions { IncludeInResponse = false }; @@ -68,7 +71,7 @@ public async Task DoesNotReturnCorrelationIdInResponseHeader_WhenOptionSetToFals } [Fact] - public async Task CorrelationIdHeaderFieldName_MatchesOptions() + public async Task CorrelationIdHeaderFieldName_MatchesHeaderOption() { const string customHeader = "X-Test-Header"; @@ -88,7 +91,7 @@ public async Task CorrelationIdHeaderFieldName_MatchesOptions() } [Fact] - public async Task CorrelationIdHeaderFieldName_MatchesStringOverload() + public async Task CorrelationIdHeaderFieldName_MatchesHeaderFromStringOverload() { const string customHeader = "X-Test-Header"; @@ -127,6 +130,46 @@ public async Task CorrelationId_SetToCorrelationIdFromRequestHeader() Assert.Single(header, expectedHeaderValue); } + [Fact] + public async Task CorrelationId_SetToGuid_WhenUseGuidForCorrelationId_IsTrue() + { + var options = new CorrelationIdOptions { UseGuidForCorrelationId = true }; + + var builder = new WebHostBuilder() + .Configure(app => app.UseCorrelationId(options)) + .ConfigureServices(sc => sc.AddCorrelationId()); + + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(""); + + var header = response.Headers.GetValues(new CorrelationIdOptions().Header); + + var isGuid = Guid.TryParse(header.FirstOrDefault(), out _); + + Assert.True(isGuid); + } + + [Fact] + public async Task CorrelationId_NotSetToGuid_WhenUseGuidForCorrelationId_IsFalse() + { + var options = new CorrelationIdOptions { UseGuidForCorrelationId = false }; + + var builder = new WebHostBuilder() + .Configure(app => app.UseCorrelationId(options)) + .ConfigureServices(sc => sc.AddCorrelationId()); + + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(""); + + var header = response.Headers.GetValues(new CorrelationIdOptions().Header); + + var isGuid = Guid.TryParse(header.FirstOrDefault(), out _); + + Assert.False(isGuid); + } + [Fact] public async Task CorrelationId_ReturnedCorrectlyFromSingletonService() { @@ -237,6 +280,100 @@ public async Task CorrelationId_ReturnedCorrectlyFromTransientService() Assert.Equal(splitContent2[0], splitContent2[1]); } + [Fact] + public async Task CorrelationContextIncludesHeaderValue_WhichMatchesTheOriginalOptionsValue() + { + var options = new CorrelationIdOptions { Header = "custom-header" }; + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCorrelationId(options); + + app.Use(async (ctx, next) => + { + var accessor = ctx.RequestServices.GetService(); + await ctx.Response.WriteAsync(accessor.CorrelationContext.Header); + await next(); + }); + }) + .ConfigureServices(sc => sc.AddCorrelationId()); + + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(""); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, options.Header); + } + + [Fact] + public async Task TraceIdentifier_IsNotUpdated_WhenUpdateTraceIdentifierIsFalse() + { + var options = new CorrelationIdOptions { UpdateTraceIdentifier = false }; + + var expectedHeaderName = new CorrelationIdOptions().Header; + const string expectedHeaderValue = "123456"; + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCorrelationId(options); + + app.Use(async (ctx, next) => + { + await ctx.Response.WriteAsync(ctx.TraceIdentifier); + await next(); + }); + }) + .ConfigureServices(sc => sc.AddCorrelationId()); + + var server = new TestServer(builder); + + var request = new HttpRequestMessage(); + request.Headers.Add(expectedHeaderName, expectedHeaderValue); + + var response = await server.CreateClient().SendAsync(request); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEqual(body, expectedHeaderValue); + } + + [Fact] + public async Task TraceIdentifier_IsNotUpdated_WhenUpdateTraceIdentifierIsTrue() + { + var options = new CorrelationIdOptions { UpdateTraceIdentifier = true }; + + var expectedHeaderName = new CorrelationIdOptions().Header; + const string expectedHeaderValue = "123456"; + + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCorrelationId(options); + + app.Use(async (ctx, next) => + { + await ctx.Response.WriteAsync(ctx.TraceIdentifier); + await next(); + }); + }) + .ConfigureServices(sc => sc.AddCorrelationId()); + + var server = new TestServer(builder); + + var request = new HttpRequestMessage(); + request.Headers.Add(expectedHeaderName, expectedHeaderValue); + + var response = await server.CreateClient().SendAsync(request); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal(body, expectedHeaderValue); + } + private class SingletonClass { private readonly ICorrelationContextAccessor _correlationContext;