diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj
index 4474733c0f..2e1bcd97bf 100644
--- a/src/AutoMapper/AutoMapper.csproj
+++ b/src/AutoMapper/AutoMapper.csproj
@@ -60,6 +60,20 @@
-->
+
+
+
+
+
+
+ $(GitCommitDate)
+
+
+
+
+
+
+
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..2fd51d4455 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?.ToLowerInvariant() 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..485ed10e5e 100644
--- a/src/AutoMapper/Licensing/LicenseValidator.cs
+++ b/src/AutoMapper/Licensing/LicenseValidator.cs
@@ -5,10 +5,17 @@ namespace AutoMapper.Licensing;
internal class LicenseValidator
{
private readonly ILogger _logger;
+ private readonly DateTimeOffset? _buildDate;
public LicenseValidator(ILoggerFactory loggerFactory)
+ : this(loggerFactory, BuildInfo.BuildDate)
+ {
+ }
+
+ public LicenseValidator(ILoggerFactory loggerFactory, DateTimeOffset? buildDate)
{
_logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License");
+ _buildDate = buildDate;
}
public void Validate(License license)
@@ -31,7 +38,31 @@ 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
+ {
+ 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.");
+ }
}
if (license.ProductType.Value != ProductType.AutoMapper
diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs
index 95ab06b3e1..496d398484 100644
--- a/src/UnitTests/Licensing/LicenseValidatorTests.cs
+++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs
@@ -110,4 +110,119 @@ 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(-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(-30).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_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()
+ {
+ 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