Skip to content

Commit

Permalink
Fix missing code generation output for custom data annotation attribu…
Browse files Browse the repository at this point in the history
…tes (#25176)

* Fixes missing code generation output for custom data annotation attributes.
* Adds regression tests.

The two regression tests currently skip the assembly build process, because the two custom data annotation attributes do not exist in the generated assembly. If necessary, the `Microsoft.EntityFrameworkCore.Design.Tests` assembly could be added as a reference or additional source code files (containing the attributes) could be added to the build process.

Fixes #25156

Co-authored-by: Laurents Meyer <laucomm@gmail.com>
  • Loading branch information
AndriySvyryd and lauxjpn committed Jul 6, 2021
1 parent 595974e commit 0e28884
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 12 deletions.
Expand Up @@ -151,6 +151,8 @@ protected virtual void GenerateEntityTypeDataAnnotations([NotNull] IEntityType e
{
attributeWriter.AddParameter(_code.UnknownLiteral(argument));
}

_sb.AppendLine(attributeWriter.ToString());
}
}

Expand Down Expand Up @@ -300,6 +302,8 @@ protected virtual void GeneratePropertyDataAnnotations([NotNull] IProperty prope
{
attributeWriter.AddParameter(_code.UnknownLiteral(argument));
}

_sb.AppendLine(attributeWriter.ToString());
}
}

Expand Down
@@ -1,8 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;

namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal
Expand Down Expand Up @@ -1402,5 +1411,277 @@ public partial class Post
Assert.Equal("OriginalPosts", originalInverseNavigation.Name);
});
}

[ConditionalFact]
public void Entity_with_custom_annotation()
{
Test(
modelBuilder => modelBuilder
.Entity(
"EntityWithAnnotation",
x =>
{
x.HasAnnotation("Custom:EntityAnnotation", "first argument");
x.Property<int>("Id");
x.HasKey("Id");
}),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
#nullable disable
namespace TestNamespace
{
[CustomEntityDataAnnotation(""first argument"")]
public partial class EntityWithAnnotation
{
[Key]
public int Id { get; set; }
}
}
",
code.AdditionalFiles.Single(f => f.Path == "EntityWithAnnotation.cs"));
AssertFileContents(
@"using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
#nullable disable
namespace TestNamespace
{
public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
public virtual DbSet<EntityWithAnnotation> EntityWithAnnotation { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning "
+ DesignStrings.SensitiveInformationWarning
+ @"
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityWithAnnotation>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
",
code.ContextFile);
},
assertModel: null,
skipBuild: true);
}

[ConditionalFact]
public void Entity_property_with_custom_annotation()
{
Test(
modelBuilder => modelBuilder
.Entity(
"EntityWithPropertyAnnotation",
x =>
{
x.Property<int>("Id")
.HasAnnotation("Custom:PropertyAnnotation", "first argument");
x.HasKey("Id");
}),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
#nullable disable
namespace TestNamespace
{
public partial class EntityWithPropertyAnnotation
{
[Key]
[CustomPropertyDataAnnotation(""first argument"")]
public int Id { get; set; }
}
}
",
code.AdditionalFiles.Single(f => f.Path == "EntityWithPropertyAnnotation.cs"));
AssertFileContents(
@"using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
#nullable disable
namespace TestNamespace
{
public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}
public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}
public virtual DbSet<EntityWithPropertyAnnotation> EntityWithPropertyAnnotation { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
#warning "
+ DesignStrings.SensitiveInformationWarning
+ @"
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityWithPropertyAnnotation>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
",
code.ContextFile);
},
assertModel: null,
skipBuild: true);
}

protected override void AddModelServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton<IRelationalAnnotationProvider, ModelAnnotationProvider>());
}

protected override void AddScaffoldingServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Singleton<IAnnotationCodeGenerator, ModelAnnotationCodeGenerator>());
}

public class ModelAnnotationProvider : SqlServerAnnotationProvider
{
public ModelAnnotationProvider(RelationalAnnotationProviderDependencies dependencies)
: base(dependencies)
{
}

/// <inheritdoc />
public override IEnumerable<IAnnotation> For(ITable table)
{
foreach (var annotation in base.For(table))
{
yield return annotation;
}

var entityType = table.EntityTypeMappings.First().EntityType;

foreach (var annotation in entityType.GetAnnotations().Where(a => a.Name == "Custom:EntityAnnotation"))
{
yield return annotation;
}
}

/// <inheritdoc />
public override IEnumerable<IAnnotation> For(IColumn column)
{
foreach (var annotation in base.For(column))
{
yield return annotation;
}

var properties = column.PropertyMappings.Select(m => m.Property);
var annotations = properties.SelectMany(p => p.GetAnnotations()).GroupBy(a => a.Name).Select(g => g.First());

foreach (var annotation in annotations.Where(a => a.Name == "Custom:PropertyAnnotation"))
{
yield return annotation;
}
}
}

public class ModelAnnotationCodeGenerator : SqlServerAnnotationCodeGenerator
{
public ModelAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies)
: base(dependencies)
{
}

protected override AttributeCodeFragment GenerateDataAnnotation(IEntityType entityType, IAnnotation annotation)
=> annotation.Name switch
{
"Custom:EntityAnnotation" => new AttributeCodeFragment(
typeof(CustomEntityDataAnnotationAttribute), new object[] { annotation.Value as string }),
_ => base.GenerateDataAnnotation(entityType, annotation)
};

protected override AttributeCodeFragment GenerateDataAnnotation(IProperty property, IAnnotation annotation)
=> annotation.Name switch
{
"Custom:PropertyAnnotation" => new AttributeCodeFragment(typeof(CustomPropertyDataAnnotationAttribute), new object[] {annotation.Value as string}),
_ => base.GenerateDataAnnotation(property, annotation)
};
}

[AttributeUsage(AttributeTargets.Class)]
public class CustomEntityDataAnnotationAttribute : Attribute
{
public CustomEntityDataAnnotationAttribute(string argument)
=> Argument = argument;

public virtual string Argument { get; }
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class CustomPropertyDataAnnotationAttribute : Attribute
{
public CustomPropertyDataAnnotationAttribute(string argument)
=> Argument = argument;

public virtual string Argument { get; }
}
}
}
Expand Up @@ -20,18 +20,24 @@ public abstract class ModelCodeGeneratorTestBase
Action<ModelBuilder> buildModel,
ModelCodeGenerationOptions options,
Action<ScaffoldedModel> assertScaffold,
Action<IModel> assertModel)
Action<IModel> assertModel,
bool skipBuild = false)
{
var modelBuilder = SqlServerTestHelpers.Instance.CreateConventionBuilder(skipValidation: true);
var designServices = new ServiceCollection();
AddModelServices(designServices);

var modelBuilder = SqlServerTestHelpers.Instance.CreateConventionBuilder(skipValidation: true, customServices: designServices);
modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
buildModel(modelBuilder);
var _ = modelBuilder.Model.GetEntityTypeErrors();

var model = modelBuilder.FinalizeModel();

var services = new ServiceCollection()
.AddEntityFrameworkDesignTimeServices();
var services = new ServiceCollection();

services.AddEntityFrameworkDesignTimeServices();
new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services);
AddScaffoldingServices(services);

var generator = services
.BuildServiceProvider()
Expand Down Expand Up @@ -60,16 +66,27 @@ public abstract class ModelCodeGeneratorTestBase
scaffoldedModel.AdditionalFiles.Select(f => f.Code)))
};

var assembly = build.BuildInMemory();
var context = (DbContext)assembly.CreateInstance("TestNamespace.TestDbContext");

if (assertModel != null)
if (!skipBuild)
{
var compiledModel = context.Model;
assertModel(compiledModel);
var assembly = build.BuildInMemory();
var context = (DbContext)assembly.CreateInstance("TestNamespace.TestDbContext");

if (assertModel != null)
{
var compiledModel = context.Model;
assertModel(compiledModel);
}
}
}

protected virtual void AddModelServices(IServiceCollection services)
{
}

protected virtual void AddScaffoldingServices(IServiceCollection services)
{
}

protected static void AssertFileContents(
string expectedCode,
ScaffoldedFile file)
Expand Down
8 changes: 6 additions & 2 deletions test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs
Expand Up @@ -135,9 +135,13 @@ public IMutableModel BuildModelFor<TEntity>()
return builder.Model;
}

public ModelBuilder CreateConventionBuilder(bool skipValidation = false)
public ModelBuilder CreateConventionBuilder(
bool skipValidation = false,
IServiceCollection customServices = null)
{
var conventionSet = CreateConventionSetBuilder().CreateConventionSet();
customServices ??= new ServiceCollection();
var contextServices = CreateContextServices(customServices);
var conventionSet = contextServices.GetRequiredService<IConventionSetBuilder>().CreateConventionSet();

if (skipValidation)
{
Expand Down

0 comments on commit 0e28884

Please sign in to comment.