From 5110be91cdfff29dc2446648eaa3d683bd0d5670 Mon Sep 17 00:00:00 2001 From: Adam Paquette Date: Sun, 4 Dec 2022 08:59:08 -0500 Subject: [PATCH 1/2] Support for convention-based max length in EF Core --- README.md | 14 +++-- .../AD.BaseTypes.EFCore.csproj | 4 +- .../ConventionSetBuilderExtensions.cs | 21 -------- .../BaseTypeConversionConvention.cs | 10 ++-- .../BaseTypeMaxLengthConvention.cs | 45 ++++++++++++++++ .../ConventionModelBuilderExtensions.cs | 19 +++++++ .../ConventionSetBuilderExtensions.cs | 51 +++++++++++++++++++ src/AD.BaseTypes/MaxLengthStringAttribute.cs | 9 ++-- .../MinMaxLengthStringAttribute.cs | 18 +++++-- src/TestApp.Web/Controllers/UserController.cs | 2 +- src/TestApp.Web/Program.cs | 2 +- src/TestApp.Web/Startup.cs | 2 +- src/TestApp/Infrastructure/AppDbContext.cs | 11 ++-- .../Infrastructure/UserConfiguration.cs | 12 ++--- .../20221201084034_InitialCreate.Designer.cs | 2 +- .../Migrations/AppDbContextModelSnapshot.cs | 2 +- 16 files changed, 160 insertions(+), 64 deletions(-) delete mode 100644 src/AD.BaseTypes.EFCore/ConventionSetBuilderExtensions.cs rename src/AD.BaseTypes.EFCore/{ => Conventions}/BaseTypeConversionConvention.cs (81%) create mode 100644 src/AD.BaseTypes.EFCore/Conventions/BaseTypeMaxLengthConvention.cs create mode 100644 src/AD.BaseTypes.EFCore/Extensions/ConventionModelBuilderExtensions.cs create mode 100644 src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs diff --git a/README.md b/README.md index f02ae79..38e4b3b 100644 --- a/README.md +++ b/README.md @@ -323,20 +323,26 @@ Do you want to use your primitives in EF Core? Check out `AD.BaseTypes.EFCore`. ### NuGetPackage PM> Install-Package AndreasDorfer.BaseTypes.EFCore -Version 1.4.0 ### Configuration -Apply a convention to your `DbContext` to tell EF Core how to save and load your primitives to the database. +Apply base type conventions to your `DbContext` to automatically configure the database. By default, the conventions will tell EF Core how to save and load your primitives and set the maximum data length. ```csharp protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { - configurationBuilder.Conventions.AddBaseTypeConversionConvention(); + configurationBuilder.Conventions.AddBaseTypeConventions(); + //OR + configurationBuilder.Conventions + .AddBaseTypeConversionConvention() + .AddBaseTypeMaxLengthConvention(); } ``` -Your can also configure your types manually +If you don't use conventions, you can configure your types manually: ```csharp builder.Property(x => x.LastName) + .HasMaxLength(LastName.MaxLength) .HasConversion>(); ``` -or overrides the default convention with a custom converter. +You can also override the default conventions: ```csharp builder.Property(x => x.FirstName) + .HasMaxLength(80) .HasConversion((x) => x + "-custom-conversion", (x) => FirstName.Create(x.Replace("-custom-conversion", ""))); ``` diff --git a/src/AD.BaseTypes.EFCore/AD.BaseTypes.EFCore.csproj b/src/AD.BaseTypes.EFCore/AD.BaseTypes.EFCore.csproj index 6c4a0e4..ebafb4a 100644 --- a/src/AD.BaseTypes.EFCore/AD.BaseTypes.EFCore.csproj +++ b/src/AD.BaseTypes.EFCore/AD.BaseTypes.EFCore.csproj @@ -17,10 +17,10 @@ - + - + diff --git a/src/AD.BaseTypes.EFCore/ConventionSetBuilderExtensions.cs b/src/AD.BaseTypes.EFCore/ConventionSetBuilderExtensions.cs deleted file mode 100644 index 31df284..0000000 --- a/src/AD.BaseTypes.EFCore/ConventionSetBuilderExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace AD.BaseTypes.EFCore; - -/// -/// Convention set builder extensions. -/// -public static class ConventionSetBuilderExtensions -{ - /// - /// Apply the value converter as a convention - /// to all properties when a model is being finalized. - /// - /// Builder for configuring conventions. - /// The - public static ConventionSetBuilder AddBaseTypeConversionConvention(this ConventionSetBuilder conventionSetBuilder) - { - conventionSetBuilder.Add(_ => new BaseTypeConversionConvention()); - return conventionSetBuilder; - } -} diff --git a/src/AD.BaseTypes.EFCore/BaseTypeConversionConvention.cs b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeConversionConvention.cs similarity index 81% rename from src/AD.BaseTypes.EFCore/BaseTypeConversionConvention.cs rename to src/AD.BaseTypes.EFCore/Conventions/BaseTypeConversionConvention.cs index 4567d06..536f69c 100644 --- a/src/AD.BaseTypes.EFCore/BaseTypeConversionConvention.cs +++ b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeConversionConvention.cs @@ -1,8 +1,8 @@ -using AD.BaseTypes.Extensions; +using AD.BaseTypes.EFCore.Extensions; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions; -namespace AD.BaseTypes.EFCore; +namespace AD.BaseTypes.EFCore.Conventions; /// /// Apply the value converter as a convention @@ -21,12 +21,8 @@ public class BaseTypeConversionConvention : IModelFinalizingConvention public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) { var baseType = typeof(IBaseType<,>); - var baseTypesProperties = modelBuilder.Metadata - .GetEntityTypes() - .SelectMany(x => x.GetDeclaredProperties()) - .Where(x => x.ClrType.ImplementsIBaseType()); - foreach (var baseTypeProperty in baseTypesProperties) + foreach (var baseTypeProperty in modelBuilder.GetBaseTypeConventionProperties()) { var wrappedType = baseTypeProperty.ClrType .GetInterfaces() diff --git a/src/AD.BaseTypes.EFCore/Conventions/BaseTypeMaxLengthConvention.cs b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeMaxLengthConvention.cs new file mode 100644 index 0000000..9b5e17b --- /dev/null +++ b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeMaxLengthConvention.cs @@ -0,0 +1,45 @@ +using AD.BaseTypes.EFCore.Extensions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace AD.BaseTypes.EFCore.Conventions; + +/// +/// Configures the maximum length of data that can be stored in all +/// properties when a model is being finalized. +/// +/// +/// Supported attributes are: +/// +/// +/// +/// +/// See Model building conventions for more information and examples. +/// +public class BaseTypeMaxLengthConvention : IModelFinalizingConvention +{ + /// + /// Called when a model is being finalized. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var baseTypeProperty in modelBuilder.GetBaseTypeConventionProperties()) + { + if (Attribute.GetCustomAttribute(baseTypeProperty.ClrType, typeof(MaxLengthStringAttribute)) is + MaxLengthStringAttribute maxLengthStringAttribute) + { + baseTypeProperty.Builder.HasMaxLength(maxLengthStringAttribute.MaxLength); + continue; + } + + if (Attribute.GetCustomAttribute(baseTypeProperty.ClrType, typeof(MinMaxLengthStringAttribute)) is + MinMaxLengthStringAttribute minMaxLengthStringAttribute) + { + baseTypeProperty.Builder.HasMaxLength(minMaxLengthStringAttribute.MaxLength); + } + } + } +} \ No newline at end of file diff --git a/src/AD.BaseTypes.EFCore/Extensions/ConventionModelBuilderExtensions.cs b/src/AD.BaseTypes.EFCore/Extensions/ConventionModelBuilderExtensions.cs new file mode 100644 index 0000000..cc7dbd9 --- /dev/null +++ b/src/AD.BaseTypes.EFCore/Extensions/ConventionModelBuilderExtensions.cs @@ -0,0 +1,19 @@ +using AD.BaseTypes.Extensions; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AD.BaseTypes.EFCore.Extensions; + +internal static class ConventionModelBuilderExtensions +{ + /// + /// Gets all non-navigation properties implementing declared on the entity type. + /// + /// Convention model builder + /// Declared non-navigation properties implementing . + public static IEnumerable GetBaseTypeConventionProperties(this IConventionModelBuilder modelBuilder) => + modelBuilder.Metadata + .GetEntityTypes() + .SelectMany(x => x.GetDeclaredProperties()) + .Where(x => x.ClrType.ImplementsIBaseType()); +} diff --git a/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs b/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs new file mode 100644 index 0000000..5cfc46d --- /dev/null +++ b/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs @@ -0,0 +1,51 @@ +using AD.BaseTypes.EFCore.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AD.BaseTypes.EFCore.Extensions; + +/// +/// Convention set builder extensions. +/// +public static class ConventionSetBuilderExtensions +{ + /// + /// Apply base type conventions to all properties when a model is being finalized. + /// + /// Builder for configuring conventions. + /// + /// Conventions applied are: + /// + /// + /// + /// + /// + /// The convention set builder + public static ConventionSetBuilder AddBaseTypeConventions(this ConventionSetBuilder conventionSetBuilder) => + conventionSetBuilder + .AddBaseTypeConversionConvention() + .AddBaseTypeMaxLengthConvention(); + + /// + /// Apply the value converter as a convention + /// to all properties when a model is being finalized. + /// + /// Builder for configuring conventions. + /// The convention set builder + public static ConventionSetBuilder AddBaseTypeConversionConvention(this ConventionSetBuilder conventionSetBuilder) + { + conventionSetBuilder.Add(_ => new BaseTypeConversionConvention()); + return conventionSetBuilder; + } + + /// + /// Configures the maximum length of data that can be stored in all + /// properties when a model is being finalized. + /// + /// Builder for configuring conventions. + /// The convention set builder + public static ConventionSetBuilder AddBaseTypeMaxLengthConvention(this ConventionSetBuilder conventionSetBuilder) + { + conventionSetBuilder.Add(_ => new BaseTypeMaxLengthConvention()); + return conventionSetBuilder; + } +} \ No newline at end of file diff --git a/src/AD.BaseTypes/MaxLengthStringAttribute.cs b/src/AD.BaseTypes/MaxLengthStringAttribute.cs index aa04a6c..348669e 100644 --- a/src/AD.BaseTypes/MaxLengthStringAttribute.cs +++ b/src/AD.BaseTypes/MaxLengthStringAttribute.cs @@ -6,14 +6,17 @@ [AttributeUsage(AttributeTargets.Class)] public class MaxLengthStringAttribute : Attribute, IBaseTypeValidation { - readonly int maxLength; + /// + /// Selected max length + /// + public int MaxLength { get; } /// Maximal length. public MaxLengthStringAttribute(int maxLength) { - this.maxLength = maxLength; + this.MaxLength = maxLength; } /// The parameter is too long. - public void Validate(string value) => StringValidation.MaxLength(maxLength, value); + public void Validate(string value) => StringValidation.MaxLength(MaxLength, value); } diff --git a/src/AD.BaseTypes/MinMaxLengthStringAttribute.cs b/src/AD.BaseTypes/MinMaxLengthStringAttribute.cs index ad0cf63..f8e959b 100644 --- a/src/AD.BaseTypes/MinMaxLengthStringAttribute.cs +++ b/src/AD.BaseTypes/MinMaxLengthStringAttribute.cs @@ -6,20 +6,28 @@ [AttributeUsage(AttributeTargets.Class)] public class MinMaxLengthStringAttribute : Attribute, IBaseTypeValidation { - readonly int minLength, maxLength; + /// + /// Selected min length + /// + public int MinLength { get; } + + /// + /// Selected max length + /// + public int MaxLength { get; } /// Minimal length. /// Maximal length. public MinMaxLengthStringAttribute(int minLength, int maxLength) { - this.minLength = minLength; - this.maxLength = maxLength; + this.MinLength = minLength; + this.MaxLength = maxLength; } /// The parameter is too short or too long. public void Validate(string value) { - StringValidation.MinLength(minLength, value); - StringValidation.MaxLength(maxLength, value); + StringValidation.MinLength(MinLength, value); + StringValidation.MaxLength(MaxLength, value); } } diff --git a/src/TestApp.Web/Controllers/UserController.cs b/src/TestApp.Web/Controllers/UserController.cs index ab749b8..6e1157e 100644 --- a/src/TestApp.Web/Controllers/UserController.cs +++ b/src/TestApp.Web/Controllers/UserController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using TestApp.Data.Infrastructure; +using TestApp.Infrastructure; using TestApp.UserAggregate; namespace TestApp.Web.Controllers; diff --git a/src/TestApp.Web/Program.cs b/src/TestApp.Web/Program.cs index 7ae5c5a..28bfdb3 100644 --- a/src/TestApp.Web/Program.cs +++ b/src/TestApp.Web/Program.cs @@ -1,4 +1,4 @@ -using TestApp.Data.Infrastructure; +using TestApp.Infrastructure; namespace TestApp.Web; diff --git a/src/TestApp.Web/Startup.cs b/src/TestApp.Web/Startup.cs index 361de4a..fae77d9 100644 --- a/src/TestApp.Web/Startup.cs +++ b/src/TestApp.Web/Startup.cs @@ -1,7 +1,7 @@ using AD.BaseTypes.ModelBinders; using AD.BaseTypes.OpenApiSchemas; using Microsoft.OpenApi.Models; -using TestApp.Data.Infrastructure; +using TestApp.Infrastructure; namespace TestApp.Web; diff --git a/src/TestApp/Infrastructure/AppDbContext.cs b/src/TestApp/Infrastructure/AppDbContext.cs index 4a2733a..0af77e6 100644 --- a/src/TestApp/Infrastructure/AppDbContext.cs +++ b/src/TestApp/Infrastructure/AppDbContext.cs @@ -1,13 +1,8 @@ -using AD.BaseTypes; -using AD.BaseTypes.EFCore; -using AD.BaseTypes.Extensions; +using AD.BaseTypes.EFCore.Extensions; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Conventions; using TestApp.UserAggregate; -namespace TestApp.Data.Infrastructure; +namespace TestApp.Infrastructure; public class AppDbContext : DbContext { @@ -33,6 +28,6 @@ protected override void OnModelCreating(ModelBuilder builder) protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { - configurationBuilder.Conventions.AddBaseTypeConversionConvention(); + configurationBuilder.Conventions.AddBaseTypeConventions(); } } diff --git a/src/TestApp/Infrastructure/UserConfiguration.cs b/src/TestApp/Infrastructure/UserConfiguration.cs index d1d10b0..8ffc3dc 100644 --- a/src/TestApp/Infrastructure/UserConfiguration.cs +++ b/src/TestApp/Infrastructure/UserConfiguration.cs @@ -2,20 +2,14 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TestApp.UserAggregate; -namespace TestApp.Data.Infrastructure; +namespace TestApp.Infrastructure; internal class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.Property(x => x.FirstName) - .HasMaxLength(FirstName.MaxLength) - .IsRequired(); - - builder.Property(x => x.LastName) - .HasMaxLength(LastName.MaxLength) - .IsRequired(); - + builder.Property(x => x.FirstName).IsRequired(); + builder.Property(x => x.LastName).IsRequired(); builder.Property(x => x.BirthDate); } } \ No newline at end of file diff --git a/src/TestApp/Migrations/20221201084034_InitialCreate.Designer.cs b/src/TestApp/Migrations/20221201084034_InitialCreate.Designer.cs index da94f71..eece834 100644 --- a/src/TestApp/Migrations/20221201084034_InitialCreate.Designer.cs +++ b/src/TestApp/Migrations/20221201084034_InitialCreate.Designer.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TestApp.Data.Infrastructure; +using TestApp.Infrastructure; #nullable disable diff --git a/src/TestApp/Migrations/AppDbContextModelSnapshot.cs b/src/TestApp/Migrations/AppDbContextModelSnapshot.cs index 8d5f3e9..49106b5 100644 --- a/src/TestApp/Migrations/AppDbContextModelSnapshot.cs +++ b/src/TestApp/Migrations/AppDbContextModelSnapshot.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TestApp.Data.Infrastructure; +using TestApp.Infrastructure; #nullable disable From 11b46e546f9045f4afca8275978f7a7f6e9f3d7a Mon Sep 17 00:00:00 2001 From: Adam Paquette Date: Sun, 4 Dec 2022 15:01:22 -0500 Subject: [PATCH 2/2] Support for convention-based IsRequired in EF Core --- README.md | 7 ++-- .../BaseTypeIsRequiredConvention.cs | 33 +++++++++++++++++++ .../ConventionSetBuilderExtensions.cs | 18 ++++++++-- .../Infrastructure/UserConfiguration.cs | 4 +-- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/AD.BaseTypes.EFCore/Conventions/BaseTypeIsRequiredConvention.cs diff --git a/README.md b/README.md index 38e4b3b..db8f4d5 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ Do you want to use your primitives in EF Core? Check out `AD.BaseTypes.EFCore`. ### NuGetPackage PM> Install-Package AndreasDorfer.BaseTypes.EFCore -Version 1.4.0 ### Configuration -Apply base type conventions to your `DbContext` to automatically configure the database. By default, the conventions will tell EF Core how to save and load your primitives and set the maximum data length. +Apply base type conventions to your `DbContext` to automatically configure the database. By default, the conventions will tell EF Core how to save and load your primitives, set the maximum data length and set IsRequied. ```csharp protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -331,18 +331,21 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura //OR configurationBuilder.Conventions .AddBaseTypeConversionConvention() - .AddBaseTypeMaxLengthConvention(); + .AddBaseTypeMaxLengthConvention() + .AddBaseTypeIsRequiredConvention(); } ``` If you don't use conventions, you can configure your types manually: ```csharp builder.Property(x => x.LastName) + .IsRequired() .HasMaxLength(LastName.MaxLength) .HasConversion>(); ``` You can also override the default conventions: ```csharp builder.Property(x => x.FirstName) + .IsRequired(false) .HasMaxLength(80) .HasConversion((x) => x + "-custom-conversion", (x) => FirstName.Create(x.Replace("-custom-conversion", ""))); ``` diff --git a/src/AD.BaseTypes.EFCore/Conventions/BaseTypeIsRequiredConvention.cs b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeIsRequiredConvention.cs new file mode 100644 index 0000000..02200b8 --- /dev/null +++ b/src/AD.BaseTypes.EFCore/Conventions/BaseTypeIsRequiredConvention.cs @@ -0,0 +1,33 @@ +using AD.BaseTypes.EFCore.Extensions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using System.Runtime.CompilerServices; + +namespace AD.BaseTypes.EFCore.Conventions; + +/// +/// Configures the properties using the required keyword as +/// required when a model is being finalized. +/// +/// +/// See Model building conventions for more information and examples. +/// +public class BaseTypeIsRequiredConvention : IModelFinalizingConvention +{ + /// + /// Called when a model is being finalized. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var baseTypeProperty in modelBuilder.GetBaseTypeConventionProperties()) + { + if (!baseTypeProperty.Builder.Metadata.IsNullable) + { + baseTypeProperty.Builder.IsRequired(true); + } + } + } +} \ No newline at end of file diff --git a/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs b/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs index 5cfc46d..126ea93 100644 --- a/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs +++ b/src/AD.BaseTypes.EFCore/Extensions/ConventionSetBuilderExtensions.cs @@ -17,13 +17,15 @@ public static class ConventionSetBuilderExtensions /// /// /// + /// /// /// /// The convention set builder - public static ConventionSetBuilder AddBaseTypeConventions(this ConventionSetBuilder conventionSetBuilder) => + public static ConventionSetBuilder AddBaseTypeConventions(this ConventionSetBuilder conventionSetBuilder) => conventionSetBuilder .AddBaseTypeConversionConvention() - .AddBaseTypeMaxLengthConvention(); + .AddBaseTypeMaxLengthConvention() + .AddBaseTypeIsRequiredConvention(); /// /// Apply the value converter as a convention @@ -48,4 +50,16 @@ public static ConventionSetBuilder AddBaseTypeMaxLengthConvention(this Conventio conventionSetBuilder.Add(_ => new BaseTypeMaxLengthConvention()); return conventionSetBuilder; } + + /// + /// Configures the properties using the required keyword as + /// required when a model is being finalized. + /// + /// Builder for configuring conventions. + /// The convention set builder + public static ConventionSetBuilder AddBaseTypeIsRequiredConvention(this ConventionSetBuilder conventionSetBuilder) + { + conventionSetBuilder.Add(_ => new BaseTypeIsRequiredConvention()); + return conventionSetBuilder; + } } \ No newline at end of file diff --git a/src/TestApp/Infrastructure/UserConfiguration.cs b/src/TestApp/Infrastructure/UserConfiguration.cs index 8ffc3dc..a7c29a5 100644 --- a/src/TestApp/Infrastructure/UserConfiguration.cs +++ b/src/TestApp/Infrastructure/UserConfiguration.cs @@ -8,8 +8,8 @@ internal class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.Property(x => x.FirstName).IsRequired(); - builder.Property(x => x.LastName).IsRequired(); + builder.Property(x => x.FirstName); + builder.Property(x => x.LastName); builder.Property(x => x.BirthDate); } } \ No newline at end of file