diff --git a/EFCore.CheckConstraints.Test/ValidationCheckConstraintTest.cs b/EFCore.CheckConstraints.Test/ValidationCheckConstraintTest.cs index 9036f50..0a3154f 100644 --- a/EFCore.CheckConstraints.Test/ValidationCheckConstraintTest.cs +++ b/EFCore.CheckConstraints.Test/ValidationCheckConstraintTest.cs @@ -192,6 +192,65 @@ public virtual void Properties_on_complex_type() Assert.Equal("[Location_Latitude] BETWEEN -90.0E0 AND 90.0E0", latitudeCheckConstraint.Sql); } + [Fact] + public void RequiredWithMaxStringLength() + { + var entityType = BuildEntityType(); + + var checkConstraint = Assert.Single(entityType.GetCheckConstraints(), c => c.Name == "CK_Blog_RequiredWithMaxStringLength_MinLength"); + Assert.NotNull(checkConstraint); + Assert.Equal("LEN([RequiredWithMaxStringLength]) >= 1", checkConstraint.Sql); + } + + [Fact] + public void RequiredWithMinAndMaxStringLength() + { + var entityType = BuildEntityType(); + + var checkConstraint = Assert.Single(entityType.GetCheckConstraints(), c => c.Name == "CK_Blog_RequiredWithMinAndMaxStringLength_MinLength"); + Assert.NotNull(checkConstraint); + Assert.Equal("LEN([RequiredWithMinAndMaxStringLength]) >= 3", checkConstraint.Sql); + } + + [Fact] + public void RequiredWithMinLength() + { + var entityType = BuildEntityType(); + + var checkConstraint = Assert.Single(entityType.GetCheckConstraints(), c => c.Name == "CK_Blog_RequiredWithMinLength_MinLength"); + Assert.NotNull(checkConstraint); + Assert.Equal("LEN([RequiredWithMinLength]) >= 3", checkConstraint.Sql); + } + + [Fact] + public void RequiredWithLength() + { + var entityType = BuildEntityType(); + + var checkConstraint = Assert.Single(entityType.GetCheckConstraints(), c => c.Name == "CK_Blog_RequiredWithLength_MinMaxLength"); + Assert.NotNull(checkConstraint); + Assert.Equal("LEN([RequiredWithLength]) BETWEEN 1 AND 4", checkConstraint.Sql); + } + + [Fact] + public void RequiredWithLengthAndStringLengthAndMinLength() + { + var entityType = BuildEntityType(); + + var checkConstraint = Assert.Single(entityType.GetCheckConstraints(), c => c.Name == "CK_Blog_RequiredWithLengthAndStringLengthAndMinLength_MinMaxLength"); + Assert.NotNull(checkConstraint); + Assert.Equal("LEN([RequiredWithLengthAndStringLengthAndMinLength]) BETWEEN 4 AND 10", checkConstraint.Sql); + } + + [Fact] + public void ShouldThrowWhenDeterminedMaxLengthIsGreaterThanMinLength() + { + var exception = Assert.Throws(() => BuildEntityType()); + Assert.Equal( + $"The minimum length (400) specified for [{nameof(MinLengthExceedingMaxLength)}].[{nameof(MinLengthExceedingMaxLength.Property)}] exceeds the maximum allowable length (10).", + exception.Message); + } + #region Support // ReSharper disable UnusedMember.Local @@ -199,6 +258,7 @@ class Blog { public int Id { get; set; } + [Required] [Range(1, 5)] public int Rating { get; set; } @@ -245,7 +305,39 @@ class Blog public required string StartsWithA { get; set; } public required Location Location { get; set; } + + [Required] + [StringLength(100)] + public required string RequiredWithMaxStringLength { get; set; } + + [Required] + [StringLength(100, MinimumLength = 3)] + public required string RequiredWithMinAndMaxStringLength { get; set; } + + [Required] + [MinLength(3)] + public required string RequiredWithMinLength { get; set; } + + [Required] + [Length(0, 4)] + public required string RequiredWithLength { get; set; } + + [Required] + [Length(0, 10)] + [StringLength(100, MinimumLength = 3)] + [MinLength(4)] + public required string RequiredWithLengthAndStringLengthAndMinLength { get; set; } + } + + class MinLengthExceedingMaxLength + { + [Required] + [Length(0, 10)] + [StringLength(100, MinimumLength = 3)] + [MinLength(400)] + public required string Property { get; set; } } + // ReSharper restore UnusedMember.Local [ComplexType] diff --git a/EFCore.CheckConstraints/Internal/ValidationCheckConstraintConvention.cs b/EFCore.CheckConstraints/Internal/ValidationCheckConstraintConvention.cs index 2b4e23e..329d91c 100644 --- a/EFCore.CheckConstraints/Internal/ValidationCheckConstraintConvention.cs +++ b/EFCore.CheckConstraints/Internal/ValidationCheckConstraintConvention.cs @@ -80,6 +80,9 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, continue; } + int? minLength = null; + int? maxLength = null; + foreach (var attribute in memberInfo.GetCustomAttributes()) { switch (attribute) @@ -89,22 +92,24 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, continue; case MinLengthAttribute a when _intTypeMapping is not null: - AddMinimumLengthConstraint(property, memberInfo, tableName, columnName, sql, a.Length); + minLength = minLength is null ? a.Length : Math.Max(a.Length, minLength.Value); continue; case StringLengthAttribute a when _intTypeMapping is not null: - AddMinimumLengthConstraint(property, memberInfo, tableName, columnName, sql, a.MinimumLength); + minLength = minLength is null ? a.MinimumLength : Math.Max(a.MinimumLength, minLength.Value); continue; - case RequiredAttribute { AllowEmptyStrings: false } when _intTypeMapping is not null: - AddMinimumLengthConstraint(property, memberInfo, tableName, columnName, sql, minLength: 1); + case RequiredAttribute { AllowEmptyStrings: false } + when _intTypeMapping is not null && memberInfo.GetMemberType() == typeof(string): + minLength = minLength is null ? 1 : Math.Max(1, minLength.Value); continue; case LengthAttribute a when _intTypeMapping is not null: // Note: The max length should be enforced by the column schema definition in EF, // see https://github.com/dotnet/efcore/issues/30754. While that isn't done, we enforce it via the check // constraint. - AddStringLengthConstraint(property, memberInfo, tableName, columnName, sql, a.MinimumLength, a.MaximumLength); + minLength = minLength is null ? a.MinimumLength : Math.Max(a.MinimumLength, minLength.Value); + maxLength = maxLength is null ? a.MaximumLength : Math.Min(a.MaximumLength, maxLength.Value); continue; case AllowedValuesAttribute a: @@ -152,6 +157,27 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, } } } + + if (minLength is null) + { + continue; + } + + if (maxLength is not null) + { + if (minLength.Value > maxLength.Value) + { + throw new InvalidOperationException( + $"The minimum length ({minLength}) specified for [{tableName}].[{columnName}] exceeds the maximum allowable length ({maxLength})." + ); + } + + AddStringLengthConstraint(property, memberInfo, tableName, columnName, sql, minLength.Value, maxLength.Value); + } + else + { + AddMinimumLengthConstraint(property, memberInfo, tableName, columnName, sql, minLength.Value); + } } } }