From ae0486f481d9c396ba8f4f4bf5b3537b41c88a15 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Mon, 23 Feb 2026 10:41:50 -0600 Subject: [PATCH 1/7] Adding perpetual licensing --- src/AutoMapper/AutoMapper.csproj | 11 +++ src/AutoMapper/Licensing/BuildInfo.cs | 26 ++++++ src/AutoMapper/Licensing/License.cs | 4 + src/AutoMapper/Licensing/LicenseValidator.cs | 25 +++++- .../Licensing/LicenseValidatorTests.cs | 87 +++++++++++++++++++ 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/AutoMapper/Licensing/BuildInfo.cs diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 4474733c0f..3af9ae34e8 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -60,6 +60,17 @@ --> + + + + $([System.DateTime]::UtcNow.ToString("O")) + + + + + + + diff --git a/src/AutoMapper/Licensing/BuildInfo.cs b/src/AutoMapper/Licensing/BuildInfo.cs new file mode 100644 index 0000000000..17e52a364f --- /dev/null +++ b/src/AutoMapper/Licensing/BuildInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace AutoMapper.Licensing; + +internal static class BuildInfo +{ + public static DateTimeOffset? BuildDate { get; } = GetBuildDate(); + + private static DateTimeOffset? GetBuildDate() + { + var assembly = typeof(BuildInfo).Assembly; + + var buildDateAttribute = assembly + .GetCustomAttributes() + .FirstOrDefault(a => a.Key == "BuildDateUtc"); + + if (buildDateAttribute?.Value != null && + DateTimeOffset.TryParse(buildDateAttribute.Value, out var buildDate)) + { + return buildDate; + } + + return null; + } +} + diff --git a/src/AutoMapper/Licensing/License.cs b/src/AutoMapper/Licensing/License.cs index 2b62092b1d..66c8b194e9 100644 --- a/src/AutoMapper/Licensing/License.cs +++ b/src/AutoMapper/Licensing/License.cs @@ -41,6 +41,9 @@ public License(ClaimsPrincipal claims) ProductType = productType; } + var perpetualValue = claims.FindFirst("perpetual")?.Value; + IsPerpetual = perpetualValue is "true" or "1"; + IsConfigured = AccountId != null && CustomerId != null && SubscriptionId != null @@ -57,6 +60,7 @@ public License(ClaimsPrincipal claims) public DateTimeOffset? ExpirationDate { get; } public Edition? Edition { get; } public ProductType? ProductType { get; } + public bool IsPerpetual { get; } public bool IsConfigured { get; } } \ No newline at end of file diff --git a/src/AutoMapper/Licensing/LicenseValidator.cs b/src/AutoMapper/Licensing/LicenseValidator.cs index 52366beaa5..803595d177 100644 --- a/src/AutoMapper/Licensing/LicenseValidator.cs +++ b/src/AutoMapper/Licensing/LicenseValidator.cs @@ -5,10 +5,12 @@ namespace AutoMapper.Licensing; internal class LicenseValidator { private readonly ILogger _logger; + private readonly DateTimeOffset? _buildDate; - public LicenseValidator(ILoggerFactory loggerFactory) + public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate = null) { _logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License"); + _buildDate = buildDate ?? BuildInfo.BuildDate; } public void Validate(License license) @@ -31,7 +33,26 @@ public void Validate(License license) var diff = DateTime.UtcNow.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays; if (diff > 0) { - errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + // If perpetual, check if build date is before expiration + if (license.IsPerpetual && _buildDate.HasValue) + { + var buildDateDiff = _buildDate.Value.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays; + if (buildDateDiff <= 0) + { + _logger.LogInformation( + "Your license for the Lucky Penny software AutoMapper expired {expiredDaysAgo} days ago, but perpetual licensing is active because the build date ({buildDate:O}) is before the license expiration date ({licenseExpiration:O}).", + diff, _buildDate, license.ExpirationDate); + // Don't add to errors - perpetual fallback applies + } + else + { + errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + } + } + else + { + errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + } } if (license.ProductType.Value != ProductType.AutoMapper diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs index 95ab06b3e1..ad81a3cee2 100644 --- a/src/UnitTests/Licensing/LicenseValidatorTests.cs +++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs @@ -110,4 +110,91 @@ public void Should_return_invalid_when_expired() logMessages .ShouldContain(log => log.Level == LogLevel.Error); } + + [Fact] + public void Should_allow_perpetual_license_when_build_date_before_expiration() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var buildDate = DateTimeOffset.UtcNow.AddDays(-30); + var licenseValidator = new LicenseValidator(factory, buildDate); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error); + logMessages.ShouldContain(log => log.Level == LogLevel.Information && + log.Message.Contains("perpetual")); + } + + [Fact] + public void Should_reject_perpetual_license_when_build_date_after_expiration() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var buildDate = DateTimeOffset.UtcNow.AddDays(30); // Build date in future (after expiration) + var licenseValidator = new LicenseValidator(factory, buildDate); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldContain(log => log.Level == LogLevel.Error && + log.Message.Contains("expired")); + } + + [Fact] + public void Should_handle_missing_perpetual_claim() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Community)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.Bundle))); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeFalse(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error + || log.Level == LogLevel.Warning + || log.Level == LogLevel.Critical); + } } \ No newline at end of file From 682e83b43b8b34476a27a71967d7da2e8a3ca50b Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 10:28:16 -0600 Subject: [PATCH 2/7] Fixing build date --- src/AutoMapper/AutoMapper.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 3af9ae34e8..17444018e0 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -62,8 +62,12 @@ --> + + + - $([System.DateTime]::UtcNow.ToString("O")) + $(GitCommitDate) + $([System.DateTime]::UtcNow.ToString("O")) From 0d027c2cd56ac87729c097334804858061bdd6ff Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 10:33:36 -0600 Subject: [PATCH 3/7] Applying feedback from MediatR --- src/AutoMapper/Licensing/LicenseValidator.cs | 5 ++++ .../Licensing/LicenseValidatorTests.cs | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/AutoMapper/Licensing/LicenseValidator.cs b/src/AutoMapper/Licensing/LicenseValidator.cs index 803595d177..1cd5cbb02a 100644 --- a/src/AutoMapper/Licensing/LicenseValidator.cs +++ b/src/AutoMapper/Licensing/LicenseValidator.cs @@ -51,6 +51,11 @@ public void Validate(License license) } else { + if (license.IsPerpetual) + { + _logger.LogWarning( + "Your license for the Lucky Penny software AutoMapper has the perpetual flag set, but the build date could not be determined. Perpetual licensing is unavailable."); + } errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); } } diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs index ad81a3cee2..7c254a7777 100644 --- a/src/UnitTests/Licensing/LicenseValidatorTests.cs +++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs @@ -170,6 +170,34 @@ public void Should_reject_perpetual_license_when_build_date_after_expiration() log.Message.Contains("expired")); } + [Fact] + public void Should_warn_and_error_when_perpetual_but_build_date_is_null() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory, buildDate: null); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-10).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), + new Claim("perpetual", "true")); + + license.IsConfigured.ShouldBeTrue(); + license.IsPerpetual.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + logMessages.ShouldContain(log => log.Level == LogLevel.Warning && log.Message.Contains("perpetual")); + logMessages.ShouldContain(log => log.Level == LogLevel.Error && log.Message.Contains("expired")); + } + [Fact] public void Should_handle_missing_perpetual_claim() { From 03f4eb32cc95c0f54ef7226bbf93979022bbd947 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 10:41:34 -0600 Subject: [PATCH 4/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/AutoMapper/Licensing/LicenseValidator.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/AutoMapper/Licensing/LicenseValidator.cs b/src/AutoMapper/Licensing/LicenseValidator.cs index 1cd5cbb02a..485ed10e5e 100644 --- a/src/AutoMapper/Licensing/LicenseValidator.cs +++ b/src/AutoMapper/Licensing/LicenseValidator.cs @@ -7,10 +7,15 @@ internal class LicenseValidator private readonly ILogger _logger; private readonly DateTimeOffset? _buildDate; - public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate = null) + public LicenseValidator(ILoggerFactory loggerFactory) + : this(loggerFactory, BuildInfo.BuildDate) + { + } + + public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate) { _logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License"); - _buildDate = buildDate ?? BuildInfo.BuildDate; + _buildDate = buildDate; } public void Validate(License license) From f7b15fb070a08e68953feb81b7e80fea1f3dfb2f Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 10:45:57 -0600 Subject: [PATCH 5/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/AutoMapper/AutoMapper.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 17444018e0..2e1bcd97bf 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -67,11 +67,10 @@ $(GitCommitDate) - $([System.DateTime]::UtcNow.ToString("O")) - + - + From 77220276d04cd52997d1f3da73201257796b0760 Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 10:46:10 -0600 Subject: [PATCH 6/7] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/UnitTests/Licensing/LicenseValidatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs index 7c254a7777..496d398484 100644 --- a/src/UnitTests/Licensing/LicenseValidatorTests.cs +++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs @@ -148,14 +148,14 @@ public void Should_reject_perpetual_license_when_build_date_after_expiration() var provider = new FakeLoggerProvider(); factory.AddProvider(provider); - var buildDate = DateTimeOffset.UtcNow.AddDays(30); // Build date in future (after expiration) + var buildDate = DateTimeOffset.UtcNow.AddDays(-1); // Build date in past, after expiration var licenseValidator = new LicenseValidator(factory, buildDate); var license = new License( new Claim("account_id", Guid.NewGuid().ToString()), new Claim("customer_id", Guid.NewGuid().ToString()), new Claim("sub_id", Guid.NewGuid().ToString()), new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), - new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeSeconds().ToString()), new Claim("edition", nameof(Edition.Professional)), new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper)), new Claim("perpetual", "true")); From 0f8f8fabf3674d2b1d93f2f516e75a29b0675aae Mon Sep 17 00:00:00 2001 From: Jimmy Bogard Date: Tue, 24 Feb 2026 11:19:37 -0600 Subject: [PATCH 7/7] Handling lower case --- src/AutoMapper/Licensing/License.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoMapper/Licensing/License.cs b/src/AutoMapper/Licensing/License.cs index 66c8b194e9..2fd51d4455 100644 --- a/src/AutoMapper/Licensing/License.cs +++ b/src/AutoMapper/Licensing/License.cs @@ -42,7 +42,7 @@ public License(ClaimsPrincipal claims) } var perpetualValue = claims.FindFirst("perpetual")?.Value; - IsPerpetual = perpetualValue is "true" or "1"; + IsPerpetual = perpetualValue?.ToLowerInvariant() is "true" or "1"; IsConfigured = AccountId != null && CustomerId != null