From a1c19e8298f0571491b926dbfe7469d62d833d8c Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Wed, 1 Jul 2026 13:13:32 -0500 Subject: [PATCH 1/2] Support license key via environment variable fallback If no license key is set in code, MediatR now resolves one from environment variables: the product-specific MEDIATR_LICENSE_KEY, then the shared LUCKYPENNY_LICENSE_KEY (usable across Lucky Penny products). Resolution uses the first non-blank value in order of precedence: explicit config key, static Mediator.LicenseKey, MEDIATR_LICENSE_KEY, then LUCKYPENNY_LICENSE_KEY. Blank values are treated as unconfigured and fall through, consistent with #1170. Ports LuckyPennySoftware/AutoMapper#4634 to MediatR. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 15 +++ src/MediatR/Licensing/LicenseAccessor.cs | 19 +++- .../LicenseKeyEnvironmentVariableTests.cs | 96 +++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs diff --git a/README.md b/README.md index 4c8d2de7..db380b8a 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,19 @@ Mediator.LicenseKey = ""; > Turn off the license warning by configuring logging in your logging start configuration: > `builder.Logging.AddFilter("LuckyPennySoftware.MediatR.License", LogLevel.None);` +#### Auto-discovery via environment variables + +If no license key is set in code, MediatR looks for one in environment variables. This is convenient for containerized and cloud environments, and for enterprises that share a single key across many services without code changes: + +- `MEDIATR_LICENSE_KEY` – the MediatR-specific license key. +- `LUCKYPENNY_LICENSE_KEY` – a shared key usable across Lucky Penny products (for example, [AutoMapper](https://github.com/LuckyPennySoftware/AutoMapper) reads the same variable). Because it is shared, the key must be for a license that includes MediatR (a `Bundle` or MediatR edition); an AutoMapper-only license will not validate here. + +The license key is resolved in the following order of precedence, using the first value found: + +1. An explicit value set in code (`cfg.LicenseKey` or `Mediator.LicenseKey`). +2. The `MEDIATR_LICENSE_KEY` environment variable. +3. The `LUCKYPENNY_LICENSE_KEY` environment variable. + +No code change is required when using an environment variable—just register MediatR as usual without setting the license key. + You can register for your license key at [MediatR.io](https://mediatr.io) diff --git a/src/MediatR/Licensing/LicenseAccessor.cs b/src/MediatR/Licensing/LicenseAccessor.cs index db1779bd..b1f8a166 100644 --- a/src/MediatR/Licensing/LicenseAccessor.cs +++ b/src/MediatR/Licensing/LicenseAccessor.cs @@ -13,6 +13,9 @@ namespace MediatR.Licensing; internal class LicenseAccessor { + internal const string MediatRLicenseKeyEnvVariable = "MEDIATR_LICENSE_KEY"; + internal const string SharedLicenseKeyEnvVariable = "LUCKYPENNY_LICENSE_KEY"; + private readonly MediatRServiceConfiguration? _configuration; private readonly ILogger _logger; @@ -41,8 +44,7 @@ private License Initialize() return _license; } - var key = _configuration?.LicenseKey - ?? Mediator.LicenseKey; + var key = ResolveLicenseKey(_configuration?.LicenseKey, Mediator.LicenseKey); if (string.IsNullOrWhiteSpace(key)) { @@ -56,6 +58,19 @@ private License Initialize() } } + /// + /// Resolves the license key using the first non-blank value, in order of precedence: the + /// explicitly configured keys (the then the + /// static ), the product-specific MEDIATR_LICENSE_KEY + /// environment variable, then the shared LUCKYPENNY_LICENSE_KEY environment variable + /// (usable across Lucky Penny products). + /// + internal static string? ResolveLicenseKey(params string?[] explicitKeys) => + explicitKeys + .Append(Environment.GetEnvironmentVariable(MediatRLicenseKeyEnvVariable)) + .Append(Environment.GetEnvironmentVariable(SharedLicenseKeyEnvVariable)) + .FirstOrDefault(key => !string.IsNullOrWhiteSpace(key)); + private Claim[] ValidateKey(string licenseKey) { var handler = new JsonWebTokenHandler(); diff --git a/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs b/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs new file mode 100644 index 00000000..b1c2c931 --- /dev/null +++ b/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs @@ -0,0 +1,96 @@ +using System; +using MediatR.Licensing; +using Shouldly; +using Xunit; + +namespace MediatR.Tests.Licensing; + +// Mutates process-global environment variables, so it must not run alongside +// other tests that read them. Disable parallelization for this class. +[Collection(nameof(LicenseKeyEnvironmentVariableTests))] +[CollectionDefinition(nameof(LicenseKeyEnvironmentVariableTests), DisableParallelization = true)] +public class LicenseKeyEnvironmentVariableTests +{ + private const string MediatREnvVar = LicenseAccessor.MediatRLicenseKeyEnvVariable; + private const string SharedEnvVar = LicenseAccessor.SharedLicenseKeyEnvVariable; + + [Fact] + public void ExplicitKey_TakesPrecedence_OverBothEnvironmentVariables() + { + const string explicitKey = "explicit-license-key"; + WithEnvironment(mediatR: "env-mediatr-key", shared: "env-shared-key", () => + LicenseAccessor.ResolveLicenseKey(explicitKey).ShouldBe(explicitKey)); + } + + [Fact] + public void ConfigurationKey_TakesPrecedence_OverStaticKey() + { + const string configKey = "config-license-key"; + WithEnvironment(mediatR: null, shared: null, () => + LicenseAccessor.ResolveLicenseKey(configKey, "static-license-key").ShouldBe(configKey)); + } + + [Fact] + public void StaticKey_Used_WhenConfigurationKeyIsBlank() + { + const string staticKey = "static-license-key"; + WithEnvironment(mediatR: null, shared: null, () => + LicenseAccessor.ResolveLicenseKey(" ", staticKey).ShouldBe(staticKey)); + } + + [Fact] + public void MediatREnvironmentVariable_Used_WhenNoExplicitKey() + { + const string mediatRKey = "env-mediatr-key"; + WithEnvironment(mediatR: mediatRKey, shared: null, () => + LicenseAccessor.ResolveLicenseKey(null, null).ShouldBe(mediatRKey)); + } + + [Fact] + public void SharedEnvironmentVariable_Used_WhenOnlyItIsSet() + { + const string sharedKey = "env-shared-key"; + WithEnvironment(mediatR: null, shared: sharedKey, () => + LicenseAccessor.ResolveLicenseKey(null, null).ShouldBe(sharedKey)); + } + + [Fact] + public void MediatREnvironmentVariable_TakesPrecedence_OverSharedEnvironmentVariable() + { + const string mediatRKey = "env-mediatr-key"; + WithEnvironment(mediatR: mediatRKey, shared: "env-shared-key", () => + LicenseAccessor.ResolveLicenseKey(null, null).ShouldBe(mediatRKey)); + } + + [Fact] + public void EnvironmentVariable_Used_WhenExplicitKeysAreBlank() + { + const string mediatRKey = "env-mediatr-key"; + WithEnvironment(mediatR: mediatRKey, shared: null, () => + LicenseAccessor.ResolveLicenseKey("", " ").ShouldBe(mediatRKey)); + } + + [Fact] + public void ReturnsNull_WhenNothingIsSet() + { + WithEnvironment(mediatR: null, shared: null, () => + LicenseAccessor.ResolveLicenseKey(null, null).ShouldBeNull()); + } + + private static void WithEnvironment(string? mediatR, string? shared, Action assert) + { + var originalMediatR = Environment.GetEnvironmentVariable(MediatREnvVar); + var originalShared = Environment.GetEnvironmentVariable(SharedEnvVar); + try + { + Environment.SetEnvironmentVariable(MediatREnvVar, mediatR); + Environment.SetEnvironmentVariable(SharedEnvVar, shared); + assert(); + } + finally + { + Environment.SetEnvironmentVariable(MediatREnvVar, originalMediatR); + Environment.SetEnvironmentVariable(SharedEnvVar, originalShared); + } + } +} From 3b345845aaadb23735e6311e1b9af4a73c255600 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Wed, 1 Jul 2026 13:53:06 -0500 Subject: [PATCH 2/2] Address Copilot review feedback - Move DisableParallelization to a dedicated collection-definition class and apply only [Collection] to the test class, matching the existing ServiceFactoryCollectionBehavior pattern. - Rewrite ResolveLicenseKey imperatively so environment variables are only read when no explicit key is configured, avoiding eager reads. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MediatR/Licensing/LicenseAccessor.cs | 18 +++++++++++++----- .../LicenseKeyEnvironmentVariableTests.cs | 8 +++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/MediatR/Licensing/LicenseAccessor.cs b/src/MediatR/Licensing/LicenseAccessor.cs index b1f8a166..d3501383 100644 --- a/src/MediatR/Licensing/LicenseAccessor.cs +++ b/src/MediatR/Licensing/LicenseAccessor.cs @@ -65,11 +65,19 @@ private License Initialize() /// environment variable, then the shared LUCKYPENNY_LICENSE_KEY environment variable /// (usable across Lucky Penny products). /// - internal static string? ResolveLicenseKey(params string?[] explicitKeys) => - explicitKeys - .Append(Environment.GetEnvironmentVariable(MediatRLicenseKeyEnvVariable)) - .Append(Environment.GetEnvironmentVariable(SharedLicenseKeyEnvVariable)) - .FirstOrDefault(key => !string.IsNullOrWhiteSpace(key)); + internal static string? ResolveLicenseKey(params string?[] explicitKeys) + { + var explicitKey = explicitKeys.FirstOrDefault(key => !string.IsNullOrWhiteSpace(key)); + if (explicitKey != null) + { + return explicitKey; + } + + return FirstConfigured(Environment.GetEnvironmentVariable(MediatRLicenseKeyEnvVariable)) + ?? FirstConfigured(Environment.GetEnvironmentVariable(SharedLicenseKeyEnvVariable)); + + static string? FirstConfigured(string? key) => string.IsNullOrWhiteSpace(key) ? null : key; + } private Claim[] ValidateKey(string licenseKey) { diff --git a/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs b/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs index b1c2c931..57325600 100644 --- a/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs +++ b/test/MediatR.Tests/Licensing/LicenseKeyEnvironmentVariableTests.cs @@ -5,10 +5,12 @@ namespace MediatR.Tests.Licensing; +[CollectionDefinition(nameof(LicenseKeyEnvironmentVariableCollection), DisableParallelization = true)] +public class LicenseKeyEnvironmentVariableCollection {} + // Mutates process-global environment variables, so it must not run alongside -// other tests that read them. Disable parallelization for this class. -[Collection(nameof(LicenseKeyEnvironmentVariableTests))] -[CollectionDefinition(nameof(LicenseKeyEnvironmentVariableTests), DisableParallelization = true)] +// other tests that read them. Disable parallelization via the collection above. +[Collection(nameof(LicenseKeyEnvironmentVariableCollection))] public class LicenseKeyEnvironmentVariableTests { private const string MediatREnvVar = LicenseAccessor.MediatRLicenseKeyEnvVariable;