In .NET 8 Preview 2 and the final release, a new LengthAttribute which is a shorter-hand for using both MinLengthAttribute and MaxLengthAttribute but not a replacement. However, this support did not show up in EFCore 8 and is yet to be added to Swashbuckle.AspNetCore. See this GitHub issue for EFCore, but I could not find any for Swashbuckle.
To make EFCore 8 pickup maximum length from the LengthAttribute, you can override the OnModelCreating(...) method ad iterate through entities and their properties but if you have this nested beyond one level, it does not work. The solution to this is a convention that can be applied to the whole context. In fact, the inbuilt support for MaxLengthAttribute is done via a convention; see the source.
We can use this as the base to build our own convention LengthAttributeConvention:
using Microsoft.EntityFrameworkCore.Diagnostics;using Microsoft.EntityFrameworkCore.Metadata.Builders;using Microsoft.EntityFrameworkCore.Metadata.Conventions;using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;using Microsoft.EntityFrameworkCore.Metadata.Internal;using System.ComponentModel.DataAnnotations;using System.ComponentModel.DataAnnotations.Schema;using System.Reflection;namespace Tingle.Extensions.EntityFrameworkCore;/// <summary>/// A convention that configures the maximum length based on the <see cref="LengthAttribute" /> applied on the property./// </summary>/// <remarks>/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information and examples./// </remarks>public class LengthAttributeConvention : PropertyAttributeConventionBase<LengthAttribute>, IComplexPropertyAddedConvention{ /// <summary> /// Creates a new instance of <see cref="LengthAttributeConvention" />. /// </summary> /// <param name="dependencies">Parameter object containing dependencies for this convention.</param> public LengthAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { } /// <inheritdoc /> protected override void ProcessPropertyAdded( IConventionPropertyBuilder propertyBuilder, LengthAttribute attribute, MemberInfo clrMember, IConventionContext context) { if (attribute.MaximumLength > 0) { propertyBuilder.HasMaxLength(attribute.MaximumLength, fromDataAnnotation: true); } } /// <inheritdoc /> protected override void ProcessPropertyAdded( IConventionComplexPropertyBuilder propertyBuilder, LengthAttribute attribute, MemberInfo clrMember, IConventionContext context) { var property = propertyBuilder.Metadata;#pragma warning disable EF1001 var member = property.GetIdentifyingMemberInfo();#pragma warning restore EF1001 if (member != null && Attribute.IsDefined(member, typeof(ForeignKeyAttribute), inherit: true)) { throw new InvalidOperationException( CoreStrings.AttributeNotOnEntityTypeProperty( "MaxLength", property.DeclaringType.DisplayName(), property.Name)); } }}
To register this I use an extension method since there are many other conventions I have:
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;using Microsoft.Extensions.DependencyInjection;using System.ComponentModel.DataAnnotations;namespace Microsoft.EntityFrameworkCore;/// <summary>Extensions for <see cref="ModelConfigurationBuilder"/>.</summary>public static class ModelConfigurationBuilderExtensions{ // omitted other conventions /// <summary> /// Add convention for handling <see cref="LengthAttribute"/>. /// </summary> /// <param name="configurationBuilder">The <see cref="ModelConfigurationBuilder"/> to use.</param> public static void AddLengthAttributeConvention(this ModelConfigurationBuilder configurationBuilder) { ArgumentNullException.ThrowIfNull(configurationBuilder); configurationBuilder.Conventions.Add(provider => new LengthAttributeConvention( provider.GetRequiredService<ProviderConventionSetBuilderDependencies>())); }}
In the DB Context:
public class MyDbContext : DbContext{ public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) {} /// <inheritdoc /> protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { base.ConfigureConventions(configurationBuilder); configurationBuilder.AddLengthAttributeConvention(); } // you can configure your model and DbSet below}
using Microsoft.OpenApi.Models;using Swashbuckle.AspNetCore.SwaggerGen;using System.ComponentModel.DataAnnotations;using System.Reflection;namespace Tingle.AspNetCore.Swagger;public class LengthAttributeSchemaFilter : ISchemaFilter{ /// <inheritdoc/> public void Apply(OpenApiSchema schema, SchemaFilterContext context) { var memberInfo = context.MemberInfo; if (memberInfo is not null) { var customAttributes = context.MemberInfo.GetInlineAndMetadataAttributes(); ApplyAttributes(schema, customAttributes); } var parameterInfo = context.ParameterInfo; if (parameterInfo is not null) { var customAttributes = parameterInfo.GetCustomAttributes(); ApplyAttributes(schema, customAttributes); } } private static void ApplyAttributes(OpenApiSchema schema, IEnumerable<object> customAttributes) { foreach (var attribute in customAttributes) { if (attribute is LengthAttribute lengthAttribute) { if (schema.Type == "array") { schema.MinItems = lengthAttribute.MinimumLength; schema.MaxItems = lengthAttribute.MaximumLength; } else { schema.MinLength = lengthAttribute.MinimumLength; schema.MaxLength = lengthAttribute.MaximumLength; } } } }}
Notice that we set MinItems and MaxItems for arrays while we set MinLength and MaxLength for the others. This matches usage for collections (e.g. IList<T>, IEnumerable<T>, etc.) and strings respectively.