diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index ab26635d4d0..0803f373a01 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -151,6 +151,8 @@ protected virtual void GenerateEntityTypeDataAnnotations([NotNull] IEntityType e { attributeWriter.AddParameter(_code.UnknownLiteral(argument)); } + + _sb.AppendLine(attributeWriter.ToString()); } } @@ -300,6 +302,8 @@ protected virtual void GeneratePropertyDataAnnotations([NotNull] IProperty prope { attributeWriter.AddParameter(_code.UnknownLiteral(argument)); } + + _sb.AppendLine(attributeWriter.ToString()); } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index c3a393e1a6f..aaead03bf79 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -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 @@ -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("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 options) + : base(options) + { + } + + public virtual DbSet 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(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("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 options) + : base(options) + { + } + + public virtual DbSet 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(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()); + } + + protected override void AddScaffoldingServices(IServiceCollection services) + { + services.Replace(ServiceDescriptor.Singleton()); + } + + public class ModelAnnotationProvider : SqlServerAnnotationProvider + { + public ModelAnnotationProvider(RelationalAnnotationProviderDependencies dependencies) + : base(dependencies) + { + } + + /// + public override IEnumerable 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; + } + } + + /// + public override IEnumerable 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; } + } } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index 213627ea0f8..f266cadeb14 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -20,18 +20,24 @@ public abstract class ModelCodeGeneratorTestBase Action buildModel, ModelCodeGenerationOptions options, Action assertScaffold, - Action assertModel) + Action 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() @@ -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) diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index 519cf26f258..bd26322aa5a 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -135,9 +135,13 @@ public IMutableModel BuildModelFor() 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().CreateConventionSet(); if (skipValidation) {