Skip to content

Commit

Permalink
Support for convention-based max length in EF Core
Browse files Browse the repository at this point in the history
  • Loading branch information
adampaquette committed Dec 4, 2022
1 parent e336d94 commit 332e2cd
Show file tree
Hide file tree
Showing 16 changed files with 160 additions and 64 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 your 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<BaseTypeValueConverter<LastName, string>>();
```
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", "")));
```
4 changes: 2 additions & 2 deletions src/AD.BaseTypes.EFCore/AD.BaseTypes.EFCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
</ItemGroup>

<ItemGroup Condition="'$(Configuration)'!='Debug'">
<PackageReference Include="AndreasDorfer.BaseTypes.Core" Version="1.4.0" />
<PackageReference Include="AndreasDorfer.BaseTypes" Version="1.4.0" />
</ItemGroup>

<ItemGroup Condition="'$(Configuration)'=='Debug'">
<ProjectReference Include="..\AD.BaseTypes.Core\AD.BaseTypes.Core.csproj" />
<ProjectReference Include="..\AD.BaseTypes\AD.BaseTypes.csproj" />
</ItemGroup>
</Project>
21 changes: 0 additions & 21 deletions src/AD.BaseTypes.EFCore/ConventionSetBuilderExtensions.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Apply the value converter <see cref="BaseTypeValueConverter{TBaseType, TWrapped}"/> as a convention
Expand All @@ -21,12 +21,8 @@ public class BaseTypeConversionConvention : IModelFinalizingConvention
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> 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()
Expand Down
45 changes: 45 additions & 0 deletions src/AD.BaseTypes.EFCore/Conventions/BaseTypeMaxLengthConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using AD.BaseTypes.EFCore.Extensions;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;

namespace AD.BaseTypes.EFCore.Conventions;

/// <summary>
/// Configures the maximum length of data that can be stored in all <see cref="T:IBaseType&lt;TBaseType,string&gt;"/>
/// properties when a model is being finalized.
/// </summary>
/// <remarks>
/// Supported attributes are:
/// <list type="bullet">
/// <item><description><see cref="MaxLengthStringAttribute"/></description></item>
/// <item><description><see cref="MinMaxLengthStringAttribute"/></description></item>
/// </list>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples.
/// </remarks>
public class BaseTypeMaxLengthConvention : IModelFinalizingConvention
{
/// <summary>
/// Called when a model is being finalized.
/// </summary>
/// <param name="modelBuilder">The builder for the model.</param>
/// <param name="context">Additional information associated with convention execution.</param>
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
IConventionContext<IConventionModelBuilder> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Gets all non-navigation properties implementing <see cref="IBaseType{TBaseType,TWrapped}"/> declared on the entity type.
/// </summary>
/// <param name="modelBuilder">Convention model builder</param>
/// <returns>Declared non-navigation properties implementing <see cref="IBaseType{TBaseType,TWrapped}"/>.</returns>
public static IEnumerable<IConventionProperty> GetBaseTypeConventionProperties(this IConventionModelBuilder modelBuilder) =>
modelBuilder.Metadata
.GetEntityTypes()
.SelectMany(x => x.GetDeclaredProperties())
.Where(x => x.ClrType.ImplementsIBaseType());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using AD.BaseTypes.EFCore.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace AD.BaseTypes.EFCore.Extensions;

/// <summary>
/// Convention set builder extensions.
/// </summary>
public static class ConventionSetBuilderExtensions
{
/// <summary>
/// Apply base type conventions to all <see cref="IBaseType{TBaseType,TWrapped}"/> properties when a model is being finalized.
/// </summary>
/// <param name="conventionSetBuilder">Builder for configuring conventions.</param>
/// <remarks>
/// Conventions applied are:
/// <list type="bullet">
/// <item><description><see cref="BaseTypeConversionConvention"/></description></item>
/// <item><description><see cref="BaseTypeMaxLengthConvention"/></description></item>
/// </list>
/// </remarks>
/// <returns>The convention set builder</returns>
public static ConventionSetBuilder AddBaseTypeConventions(this ConventionSetBuilder conventionSetBuilder) =>
conventionSetBuilder
.AddBaseTypeConversionConvention()
.AddBaseTypeMaxLengthConvention();

/// <summary>
/// Apply the value converter <see cref="BaseTypeValueConverter{TBaseType, TWrapped}"/> as a convention
/// to all <see cref="IBaseType{TBaseType,TWrapped}"/> properties when a model is being finalized.
/// </summary>
/// <param name="conventionSetBuilder">Builder for configuring conventions.</param>
/// <returns>The convention set builder</returns>
public static ConventionSetBuilder AddBaseTypeConversionConvention(this ConventionSetBuilder conventionSetBuilder)
{
conventionSetBuilder.Add(_ => new BaseTypeConversionConvention());
return conventionSetBuilder;
}

/// <summary>
/// Configures the maximum length of data that can be stored in all<see cref="T:IBaseType&lt;TBaseType,string&gt;"/>
/// properties when a model is being finalized.
/// </summary>
/// <param name="conventionSetBuilder">Builder for configuring conventions.</param>
/// <returns>The convention set builder</returns>
public static ConventionSetBuilder AddBaseTypeMaxLengthConvention(this ConventionSetBuilder conventionSetBuilder)
{
conventionSetBuilder.Add(_ => new BaseTypeMaxLengthConvention());
return conventionSetBuilder;
}
}
9 changes: 6 additions & 3 deletions src/AD.BaseTypes/MaxLengthStringAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
[AttributeUsage(AttributeTargets.Class)]
public class MaxLengthStringAttribute : Attribute, IBaseTypeValidation<string>
{
readonly int maxLength;
/// <summary>
/// Selected max length
/// </summary>
public int MaxLength { get; }

/// <param name="maxLength">Maximal length.</param>
public MaxLengthStringAttribute(int maxLength)
{
this.maxLength = maxLength;
this.MaxLength = maxLength;
}

/// <exception cref="ArgumentOutOfRangeException">The parameter <paramref name="value"/> is too long.</exception>
public void Validate(string value) => StringValidation.MaxLength(maxLength, value);
public void Validate(string value) => StringValidation.MaxLength(MaxLength, value);
}
18 changes: 13 additions & 5 deletions src/AD.BaseTypes/MinMaxLengthStringAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@
[AttributeUsage(AttributeTargets.Class)]
public class MinMaxLengthStringAttribute : Attribute, IBaseTypeValidation<string>
{
readonly int minLength, maxLength;
/// <summary>
/// Selected min length
/// </summary>
public int MinLength { get; }

/// <summary>
/// Selected max length
/// </summary>
public int MaxLength { get; }

/// <param name="minLength">Minimal length.</param>
/// <param name="maxLength">Maximal length.</param>
public MinMaxLengthStringAttribute(int minLength, int maxLength)
{
this.minLength = minLength;
this.maxLength = maxLength;
this.MinLength = minLength;
this.MaxLength = maxLength;
}

/// <exception cref="ArgumentOutOfRangeException">The parameter <paramref name="value"/> is too short or too long.</exception>
public void Validate(string value)
{
StringValidation.MinLength(minLength, value);
StringValidation.MaxLength(maxLength, value);
StringValidation.MinLength(MinLength, value);
StringValidation.MaxLength(MaxLength, value);
}
}
2 changes: 1 addition & 1 deletion src/TestApp.Web/Controllers/UserController.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/TestApp.Web/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using TestApp.Data.Infrastructure;
using TestApp.Infrastructure;

namespace TestApp.Web;

Expand Down
2 changes: 1 addition & 1 deletion src/TestApp.Web/Startup.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
11 changes: 3 additions & 8 deletions src/TestApp/Infrastructure/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -33,6 +28,6 @@ protected override void OnModelCreating(ModelBuilder builder)

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.AddBaseTypeConversionConvention();
configurationBuilder.Conventions.AddBaseTypeConventions();
}
}
12 changes: 3 additions & 9 deletions src/TestApp/Infrastructure/UserConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TestApp.UserAggregate;

namespace TestApp.Data.Infrastructure;
namespace TestApp.Infrastructure;

internal class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> 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);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/TestApp/Migrations/AppDbContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 332e2cd

Please sign in to comment.