Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for convention-based max length in EF Core #25

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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<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