Skip to content

Commit

Permalink
Add complex type support to migrations
Browse files Browse the repository at this point in the history
Add model validation rules for complex types
Discover public fields on complex types
Add support for ComplexTypeAttribute

Part of #13947
  • Loading branch information
AndriySvyryd authored Jul 12, 2023
1 parent f67dbc8 commit bb1fb3c
Show file tree
Hide file tree
Showing 62 changed files with 1,646 additions and 954 deletions.
153 changes: 128 additions & 25 deletions src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ namespace Microsoft.EntityFrameworkCore.Migrations.Design;
public class CSharpSnapshotGenerator : ICSharpSnapshotGenerator
{
private static readonly MethodInfo HasAnnotationMethodInfo
= typeof(ModelBuilder).GetRuntimeMethod(nameof(ModelBuilder.HasAnnotation), new[] { typeof(string), typeof(string) })!;
= typeof(ModelBuilder).GetRuntimeMethod(nameof(ModelBuilder.HasAnnotation),
new[] { typeof(string), typeof(string) })!;

private static readonly MethodInfo HasPropertyAnnotationMethodInfo
= typeof(ComplexPropertyBuilder).GetRuntimeMethod(nameof(ComplexPropertyBuilder.HasPropertyAnnotation),
new[] { typeof(string), typeof(string) })!;

private static readonly MethodInfo HasTypeAnnotationMethodInfo
= typeof(ComplexPropertyBuilder).GetRuntimeMethod(nameof(ComplexPropertyBuilder.HasTypeAnnotation),
new[] { typeof(string), typeof(string) })!;

/// <summary>
/// Initializes a new instance of the <see cref="CSharpSnapshotGenerator" /> class.
Expand Down Expand Up @@ -102,11 +111,11 @@ protected virtual void GenerateEntityTypes(
/// <summary>
/// Generates code for an <see cref="IEntityType" />.
/// </summary>
/// <param name="modelBuilderName">The name of the builder variable.</param>
/// <param name="builderName">The name of the builder variable.</param>
/// <param name="entityType">The entity type.</param>
/// <param name="stringBuilder">The builder code is added to.</param>
protected virtual void GenerateEntityType(
string modelBuilderName,
string builderName,
IEntityType entityType,
IndentedStringBuilder stringBuilder)
{
Expand All @@ -121,10 +130,10 @@ protected virtual void GenerateEntityType(
entityTypeName = entityType.ClrType.DisplayName();
}

var entityTypeBuilderName = GenerateEntityTypeBuilderName();
var entityTypeBuilderName = GenerateNestedBuilderName(builderName);

stringBuilder
.Append(modelBuilderName)
.Append(builderName)
.Append(
ownerNavigation != null
? ownership!.IsUnique ? ".OwnsOne(" : ".OwnsMany("
Expand Down Expand Up @@ -153,6 +162,8 @@ protected virtual void GenerateEntityType(

GenerateProperties(entityTypeBuilderName, entityType.GetDeclaredProperties(), stringBuilder);

GenerateComplexProperties(entityTypeBuilderName, entityType.GetDeclaredComplexProperties(), stringBuilder);

GenerateKeys(
entityTypeBuilderName,
entityType.GetDeclaredKeys(),
Expand All @@ -179,24 +190,6 @@ protected virtual void GenerateEntityType(
stringBuilder
.AppendLine("});");
}

string GenerateEntityTypeBuilderName()
{
if (modelBuilderName.StartsWith("b", StringComparison.Ordinal))
{
// ReSharper disable once InlineOutVariableDeclaration
var counter = 1;
if (modelBuilderName.Length > 1
&& int.TryParse(modelBuilderName[1..], out counter))
{
counter++;
}

return "b" + (counter == 0 ? "" : counter.ToString());
}

return "b";
}
}

/// <summary>
Expand Down Expand Up @@ -527,6 +520,114 @@ protected virtual void GeneratePropertyAnnotations(
?? (property.FindTypeMapping()
?? Dependencies.RelationalTypeMappingSource.FindMapping(property))?.Converter;

/// <summary>
/// Generates code for <see cref="IComplexProperty" /> objects.
/// </summary>
/// <param name="typeBuilderName">The name of the builder variable.</param>
/// <param name="properties">The properties.</param>
/// <param name="stringBuilder">The builder code is added to.</param>
protected virtual void GenerateComplexProperties(
string typeBuilderName,
IEnumerable<IComplexProperty> properties,
IndentedStringBuilder stringBuilder)
{
foreach (var property in properties)
{
GenerateComplexProperty(typeBuilderName, property, stringBuilder);
}
}

/// <summary>
/// Generates code for an <see cref="IComplexProperty" />.
/// </summary>
/// <param name="builderName">The name of the builder variable.</param>
/// <param name="complexProperty">The entity type.</param>
/// <param name="stringBuilder">The builder code is added to.</param>
protected virtual void GenerateComplexProperty(
string builderName,
IComplexProperty complexProperty,
IndentedStringBuilder stringBuilder)
{
var complexType = complexProperty.ComplexType;
var complexTypeBuilderName = GenerateNestedBuilderName(builderName);

stringBuilder
.AppendLine()
.Append(builderName)
.Append($".ComplexProperty<{Code.Reference(Model.DefaultPropertyBagType)}>(")
.Append($"{Code.Literal(complexProperty.Name)}, {Code.Literal(complexType.Name)}, ")
.Append(complexTypeBuilderName)
.AppendLine(" =>");

using (stringBuilder.Indent())
{
stringBuilder.Append("{");

using (stringBuilder.Indent())
{
if (complexProperty.IsNullable != complexProperty.ClrType.IsNullableType())
{
stringBuilder
.AppendLine()
.Append(".IsRequired()");
}

GenerateProperties(complexTypeBuilderName, complexType.GetDeclaredProperties(), stringBuilder);

GenerateComplexProperties(complexTypeBuilderName, complexType.GetDeclaredComplexProperties(), stringBuilder);

GenerateComplexPropertyAnnotations(complexTypeBuilderName, complexProperty, stringBuilder);
}

stringBuilder
.AppendLine("});");
}
}

private static string GenerateNestedBuilderName(string builderName)
{
if (builderName.StartsWith("b", StringComparison.Ordinal))
{
// ReSharper disable once InlineOutVariableDeclaration
var counter = 1;
if (builderName.Length > 1
&& int.TryParse(builderName[1..], out counter))
{
counter++;
}

return "b" + (counter == 0 ? "" : counter.ToString());
}

return "b";
}

/// <summary>
/// Generates code for the annotations on an <see cref="IProperty" />.
/// </summary>
/// <param name="propertyBuilderName">The name of the builder variable.</param>
/// <param name="property">The property.</param>
/// <param name="stringBuilder">The builder code is added to.</param>
protected virtual void GenerateComplexPropertyAnnotations(
string propertyBuilderName,
IComplexProperty property,
IndentedStringBuilder stringBuilder)
{
var propertyAnnotations = Dependencies.AnnotationCodeGenerator
.FilterIgnoredAnnotations(property.GetAnnotations())
.ToDictionary(a => a.Name, a => a);

var typeAnnotations = Dependencies.AnnotationCodeGenerator
.FilterIgnoredAnnotations(property.ComplexType.GetAnnotations())
.ToDictionary(a => a.Name, a => a);

GenerateAnnotations(propertyBuilderName, property, stringBuilder, propertyAnnotations,
inChainedCall: false, hasAnnotationMethodInfo: HasPropertyAnnotationMethodInfo);

GenerateAnnotations(propertyBuilderName, property, stringBuilder, typeAnnotations,
inChainedCall: false, hasAnnotationMethodInfo: HasTypeAnnotationMethodInfo);
}

/// <summary>
/// Generates code for <see cref="IKey" /> objects.
/// </summary>
Expand Down Expand Up @@ -1763,7 +1864,8 @@ private void GenerateAnnotations(
IndentedStringBuilder stringBuilder,
Dictionary<string, IAnnotation> annotations,
bool inChainedCall,
bool leadingNewline = true)
bool leadingNewline = true,
MethodInfo? hasAnnotationMethodInfo = null)
{
var fluentApiCalls = Dependencies.AnnotationCodeGenerator.GenerateFluentApiCalls(annotatable, annotations);

Expand All @@ -1789,7 +1891,8 @@ private void GenerateAnnotations(
// Append remaining raw annotations which did not get generated as Fluent API calls
foreach (var annotation in annotations.Values.OrderBy(a => a.Name))
{
var call = new MethodCallCodeFragment(HasAnnotationMethodInfo, annotation.Name, annotation.Value);
var call = new MethodCallCodeFragment(hasAnnotationMethodInfo ?? HasAnnotationMethodInfo,
annotation.Name, annotation.Value);
chainedCall = chainedCall is null ? call : chainedCall.Chain(call);
}

Expand Down
14 changes: 12 additions & 2 deletions src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Migrations.Design;

/// <summary>
Expand Down Expand Up @@ -171,10 +174,17 @@ private static IEnumerable<IAnnotatable> GetAnnotatables(IEnumerable<MigrationOp
/// <returns>The namespaces.</returns>
protected virtual IEnumerable<string> GetNamespaces(IModel model)
=> model.GetEntityTypes().SelectMany(
e => e.GetDeclaredProperties()
.SelectMany(p => (FindValueConverter(p)?.ProviderClrType ?? p.ClrType).GetNamespaces()))
e => GetNamespaces(e)
.Concat(e.GetDeclaredComplexProperties().Any()
? Model.DefaultPropertyBagType.GetNamespaces()
: Enumerable.Empty<string>()))
.Concat(GetAnnotationNamespaces(GetAnnotatables(model)));

private IEnumerable<string> GetNamespaces(ITypeBase typeBase)
=> typeBase.GetDeclaredProperties()
.SelectMany(p => (FindValueConverter(p)?.ProviderClrType ?? p.ClrType).GetNamespaces())
.Concat(typeBase.GetDeclaredComplexProperties().SelectMany(p => GetNamespaces(p.ComplexType)));

private static IEnumerable<IAnnotatable> GetAnnotatables(IModel model)
{
yield return model;
Expand Down
88 changes: 88 additions & 0 deletions src/EFCore.Relational/Design/AnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ public virtual void RemoveAnnotationsHandledByConventions(
RemoveConventionalAnnotationsHelper(entityType, annotations, IsHandledByConvention);
}

/// <inheritdoc />
public virtual void RemoveAnnotationsHandledByConventions(
IComplexType complexType,
IDictionary<string, IAnnotation> annotations)
=> RemoveConventionalAnnotationsHelper(complexType, annotations, IsHandledByConvention);

/// <inheritdoc />
public virtual void RemoveAnnotationsHandledByConventions(
IEntityTypeMappingFragment fragment,
Expand All @@ -107,6 +113,12 @@ public virtual void RemoveAnnotationsHandledByConventions(
RemoveConventionalAnnotationsHelper(property, annotations, IsHandledByConvention);
}

/// <inheritdoc />
public virtual void RemoveAnnotationsHandledByConventions(
IComplexProperty complexProperty,
IDictionary<string, IAnnotation> annotations)
=> RemoveConventionalAnnotationsHelper(complexProperty, annotations, IsHandledByConvention);

/// <inheritdoc />
public virtual void RemoveAnnotationsHandledByConventions(IKey key, IDictionary<string, IAnnotation> annotations)
{
Expand Down Expand Up @@ -236,6 +248,18 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
return methodCallCodeFragments;
}

/// <inheritdoc />
public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IComplexType complexType,
IDictionary<string, IAnnotation> annotations)
{
var methodCallCodeFragments = new List<MethodCallCodeFragment>();

methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexType, annotations, GenerateFluentApi));

return methodCallCodeFragments;
}

/// <inheritdoc />
public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IEntityTypeMappingFragment fragment,
Expand Down Expand Up @@ -315,6 +339,18 @@ public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
return methodCallCodeFragments;
}

/// <inheritdoc />
public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IComplexProperty complexProperty,
IDictionary<string, IAnnotation> annotations)
{
var methodCallCodeFragments = new List<MethodCallCodeFragment>();

methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexProperty, annotations, GenerateFluentApi));

return methodCallCodeFragments;
}

/// <inheritdoc />
public virtual IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IKey key,
Expand Down Expand Up @@ -512,6 +548,19 @@ protected virtual bool IsHandledByConvention(IModel model, IAnnotation annotatio
protected virtual bool IsHandledByConvention(IEntityType entityType, IAnnotation annotation)
=> false;

/// <summary>
/// Checks if the given <paramref name="annotation" /> is handled by convention when
/// applied to the given <paramref name="complexType" />.
/// </summary>
/// <remarks>
/// The default implementation always returns <see langword="false" />.
/// </remarks>
/// <param name="complexType">The <see cref="IComplexType" />.</param>
/// <param name="annotation">The <see cref="IAnnotation" />.</param>
/// <returns><see langword="false" />.</returns>
protected virtual bool IsHandledByConvention(IComplexType complexType, IAnnotation annotation)
=> false;

/// <summary>
/// Checks if the given <paramref name="annotation" /> is handled by convention when
/// applied to the given <paramref name="fragment" />.
Expand Down Expand Up @@ -551,6 +600,19 @@ protected virtual bool IsHandledByConvention(IKey key, IAnnotation annotation)
protected virtual bool IsHandledByConvention(IProperty property, IAnnotation annotation)
=> false;

/// <summary>
/// Checks if the given <paramref name="annotation" /> is handled by convention when
/// applied to the given <paramref name="complexProperty" />.
/// </summary>
/// <remarks>
/// The default implementation always returns <see langword="false" />.
/// </remarks>
/// <param name="complexProperty">The <see cref="IComplexProperty" />.</param>
/// <param name="annotation">The <see cref="IAnnotation" />.</param>
/// <returns><see langword="false" />.</returns>
protected virtual bool IsHandledByConvention(IComplexProperty complexProperty, IAnnotation annotation)
=> false;

/// <summary>
/// Checks if the given <paramref name="annotation" /> is handled by convention when
/// applied to the given <paramref name="foreignKey" />.
Expand Down Expand Up @@ -681,6 +743,19 @@ protected virtual bool IsHandledByConvention(ISequence sequence, IAnnotation ann
protected virtual MethodCallCodeFragment? GenerateFluentApi(IEntityType entityType, IAnnotation annotation)
=> null;

/// <summary>
/// Returns a fluent API call for the given <paramref name="annotation" />, or <see langword="null" />
/// if no fluent API call exists for it.
/// </summary>
/// <remarks>
/// The default implementation always returns <see langword="null" />.
/// </remarks>
/// <param name="complexType">The <see cref="IComplexType" />.</param>
/// <param name="annotation">The <see cref="IAnnotation" />.</param>
/// <returns><see langword="null" />.</returns>
protected virtual MethodCallCodeFragment? GenerateFluentApi(IComplexType complexType, IAnnotation annotation)
=> null;

/// <summary>
/// Returns a fluent API call for the given <paramref name="annotation" />, or <see langword="null" />
/// if no fluent API call exists for it.
Expand Down Expand Up @@ -720,6 +795,19 @@ protected virtual bool IsHandledByConvention(ISequence sequence, IAnnotation ann
protected virtual MethodCallCodeFragment? GenerateFluentApi(IProperty property, IAnnotation annotation)
=> null;

/// <summary>
/// Returns a fluent API call for the given <paramref name="annotation" />, or <see langword="null" />
/// if no fluent API call exists for it.
/// </summary>
/// <remarks>
/// The default implementation always returns <see langword="null" />.
/// </remarks>
/// <param name="complexProperty">The <see cref="IProperty" />.</param>
/// <param name="annotation">The <see cref="IAnnotation" />.</param>
/// <returns><see langword="null" />.</returns>
protected virtual MethodCallCodeFragment? GenerateFluentApi(IComplexProperty complexProperty, IAnnotation annotation)
=> null;

/// <summary>
/// Returns a fluent API call for the given <paramref name="annotation" />, or <see langword="null" />
/// if no fluent API call exists for it.
Expand Down
Loading

0 comments on commit bb1fb3c

Please sign in to comment.