diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 24a65d52d0d..55670a8ca25 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -572,6 +572,10 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) { // Strip out any annotations handled as attributes - these are already handled when generating // the entity's properties + // Only relational ones need to be removed here. Core ones are already removed by FilterIgnoredAnnotations + annotations.Remove(RelationalAnnotationNames.ColumnName); + annotations.Remove(RelationalAnnotationNames.ColumnType); + _ = _annotationCodeGenerator.GenerateDataAnnotationAttributes(property, annotations); } else @@ -640,7 +644,8 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations) var valueGenerated = property.ValueGenerated; var isRowVersion = false; - if (((IConventionProperty)property).GetValueGeneratedConfigurationSource().HasValue + if (((IConventionProperty)property).GetValueGeneratedConfigurationSource() is ConfigurationSource valueGeneratedConfigurationSource + && valueGeneratedConfigurationSource != ConfigurationSource.Convention && RelationalValueGenerationConvention.GetValueGenerated(property) != valueGenerated) { var methodName = valueGenerated switch diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index 6eb0a475204..a4adfa6d485 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -23,7 +23,7 @@ public void Empty_model() new ModelCodeGenerationOptions(), code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -63,8 +63,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); Assert.Empty(code.AdditionalFiles); }, @@ -79,7 +78,7 @@ public void SuppressConnectionStringWarning_works() new ModelCodeGenerationOptions { SuppressConnectionStringWarning = true }, code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -116,8 +115,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); Assert.Empty(code.AdditionalFiles); }, @@ -132,7 +130,7 @@ public void SuppressOnConfiguring_works() new ModelCodeGenerationOptions { SuppressOnConfiguring = true }, code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -161,11 +159,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); Assert.Empty(code.AdditionalFiles); - }); + }, + null); } [ConditionalFact] @@ -448,7 +446,7 @@ public void Entity_with_indexes_and_use_data_annotations_false_always_generates_ new ModelCodeGenerationOptions { UseDataAnnotations = false }, code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -502,8 +500,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); }, model => Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); @@ -532,7 +529,7 @@ public void Entity_with_indexes_and_use_data_annotations_true_generates_fluent_A new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -583,8 +580,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); }, model => Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); @@ -616,7 +612,7 @@ public void Entity_lambda_uses_correct_identifiers() new ModelCodeGenerationOptions { UseDataAnnotations = false }, code => { - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -659,8 +655,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Id).UseIdentityColumn(); - entity.Property(e => e.DependentId).ValueGeneratedNever(); - entity.HasOne(d => d.NavigationToPrincipal) .WithOne(p => p.NavigationToDependent) .HasPrincipalKey(p => p.PrincipalId) @@ -672,8 +666,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasKey(e => e.AlternateId); entity.Property(e => e.AlternateId).UseIdentityColumn(); - - entity.Property(e => e.Id).ValueGeneratedNever(); }); OnModelCreatingPartial(modelBuilder); @@ -683,8 +675,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, - ignoreLineEndingDifferences: true); + code.ContextFile); }, model => { }); } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index d0642dda3f0..6c47f00144f 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using Microsoft.EntityFrameworkCore.Internal; using Xunit; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal @@ -9,30 +10,659 @@ namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal public class CSharpEntityTypeGeneratorTest : ModelCodeGeneratorTestBase { [ConditionalFact] - public void Navigation_properties() + public void KeylessAttribute_is_generated_for_key_less_entity() + { + Test( + modelBuilder => modelBuilder.Entity("Vista").HasNoKey(), + 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 +{ + [Keyless] + public partial class Vista + { + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Vista.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 Vista { 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) + { + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Vista"); + Assert.Null(entityType.FindPrimaryKey()); + }); + } + + [ConditionalFact] + public void TableAttribute_is_generated_for_custom_name() + { + Test( + modelBuilder => + { + modelBuilder.Entity("Vista", + b => + { + b.ToTable("Vistas"); // Default name is "Vista" in the absence of pluralizer + b.Property("Id"); + b.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 +{ + [Table(""Vistas"")] + public partial class Vista + { + [Key] + public int Id { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Vista.cs")); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Vista"); + Assert.Equal("Vistas", entityType.GetTableName()); + Assert.Null(entityType.GetSchema()); + }); + } + + [ConditionalFact] + public void TableAttribute_is_not_generated_for_default_schema() + { + Test( + modelBuilder => + { + modelBuilder.HasDefaultSchema("dbo"); + modelBuilder.Entity("Vista", + b => + { + b.ToTable("Vista", "dbo"); // Default name is "Vista" in the absence of pluralizer + b.Property("Id"); + b.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 Vista + { + [Key] + public int Id { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Vista.cs")); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Vista"); + Assert.Equal("Vista", entityType.GetTableName()); + Assert.Null(entityType.GetSchema()); // Takes through model default schema + }); + } + + [ConditionalFact] + public void TableAttribute_is_generated_for_non_default_schema() + { + Test( + modelBuilder => + { + modelBuilder.HasDefaultSchema("dbo"); + modelBuilder.Entity("Vista", + b => + { + b.ToTable("Vista", "custom"); + b.Property("Id"); + b.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 +{ + [Table(""Vista"", Schema = ""custom"")] + public partial class Vista + { + [Key] + public int Id { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Vista.cs")); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Vista"); + Assert.Equal("Vista", entityType.GetTableName()); + Assert.Equal("custom", entityType.GetSchema()); + }); + } + + [ConditionalFact] + public void TableAttribute_is_not_generated_for_views() + { + Test( + modelBuilder => modelBuilder.Entity("Vista").ToView("Vistas", "dbo"), + 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 +{ + [Keyless] + public partial class Vista + { + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Vista.cs")); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.Vista"); + Assert.Equal("Vistas", entityType.GetViewName()); + Assert.Null(entityType.GetTableName()); + Assert.Equal("dbo", entityType.GetViewSchema()); + Assert.Null(entityType.GetSchema()); + }); + } + + [ConditionalFact] + public void IndexAttribute_is_generated_for_multiple_indexes_with_name_unique() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB") + .IsUnique(); + x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC"); + x.HasIndex("C"); + }), + 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 +{ + [Index(nameof(C))] + [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] + [Index(nameof(B), nameof(C), Name = ""IndexOnBAndC"")] + public partial class EntityWithIndexes + { + [Key] + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "EntityWithIndexes.cs")); + }, + model => + { + var entityType = model.FindEntityType("TestNamespace.EntityWithIndexes"); + var indexes = entityType.GetIndexes(); + Assert.Collection(indexes, + t => Assert.Null(t.Name), + t => Assert.Equal("IndexOnAAndB", t.Name), + t => Assert.Equal("IndexOnBAndC", t.Name)); + }); + } + + [ConditionalFact] + public void Entity_with_indexes_generates_IndexAttribute_only_for_indexes_without_annotations() + { + Test( + modelBuilder => modelBuilder + .Entity( + "EntityWithIndexes", + x => + { + x.Property("Id"); + x.Property("A"); + x.Property("B"); + x.Property("C"); + x.HasKey("Id"); + x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB") + .IsUnique(); + x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC") + .HasFilter("Filter SQL"); + }), + 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 +{ + [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] + public partial class EntityWithIndexes + { + [Key] + public int Id { get; set; } + public int A { get; set; } + public int B { get; set; } + public int C { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "EntityWithIndexes.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 EntityWithIndexes { 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.HasIndex(e => new { e.B, e.C }, ""IndexOnBAndC"") + .HasFilter(""Filter SQL""); + + entity.Property(e => e.Id).UseIdentityColumn(); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile); + }, + model => + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + } + + [ConditionalFact] + public void KeyAttribute_is_generated_for_single_property_and_no_fluent_api() { Test( modelBuilder => modelBuilder .Entity( - "Person", - x => x.Property("Id")) + "Entity", + x => + { + x.Property("PrimaryKey"); + x.HasKey("PrimaryKey"); + }), + 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 Entity + { + [Key] + public int PrimaryKey { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Entity.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 Entity { 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.PrimaryKey).UseIdentityColumn(); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile); + }, + model => + Assert.Equal("PrimaryKey", model.FindEntityType("TestNamespace.Entity").FindPrimaryKey().Properties[0].Name)); + } + + [ConditionalFact] + public void KeyAttribute_is_generated_on_multiple_properties_but_configuring_using_fluent_api_for_composite_key() + { + Test( + modelBuilder => modelBuilder .Entity( - "Contribution", - x => x.Property("Id")) + "Post", + x => + { + x.Property("Key"); + x.Property("Serial"); + x.HasKey("Key", "Serial"); + }), + 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 Post + { + [Key] + public int Key { get; set; } + [Key] + public int Serial { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Post.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 Post { 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.HasKey(e => new { e.Key, e.Serial }); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} +", + code.ContextFile); + }, + model => + { + var postType = model.FindEntityType("TestNamespace.Post"); + Assert.Equal(new[] { "Key", "Serial" }, postType.FindPrimaryKey().Properties.Select(p => p.Name)); + }); + } + + [ConditionalFact] + public void RequiredAttribute_is_generated_for_property() + { + Test( + modelBuilder => modelBuilder + .Entity( + "Entity", + x => + { + x.Property("Id"); + x.Property("RequiredString").IsRequired(); + }), + 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 Entity + { + [Key] + public int Id { get; set; } + [Required] + public string RequiredString { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); + }, + model => + Assert.False(model.FindEntityType("TestNamespace.Entity").GetProperty("RequiredString").IsNullable)); + } + + [ConditionalFact] + public void RequiredAttribute_is_not_generated_for_key_property() + { + Test( + modelBuilder => modelBuilder .Entity( - "Post", + "Entity", x => { - x.Property("Id"); - - x.HasOne("Person", "Author").WithMany("Posts"); - x.HasMany("Contribution", "Contributions").WithOne(); + x.Property("RequiredString"); + x.HasKey("RequiredString"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var postFile = code.AdditionalFiles.First(f => f.Path == "Post.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -43,59 +673,39 @@ public void Navigation_properties() namespace TestNamespace { - public partial class Post + public partial class Entity { - public Post() - { - Contributions = new HashSet(); - } - [Key] - public int Id { get; set; } - public int? AuthorId { get; set; } - - [ForeignKey(nameof(AuthorId))] - [InverseProperty(nameof(Person.Posts))] - public virtual Person Author { get; set; } - public virtual ICollection Contributions { get; set; } + public string RequiredString { get; set; } } } ", - postFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); }, model => - { - var postType = model.FindEntityType("TestNamespace.Post"); - var authorNavigation = postType.FindNavigation("Author"); - Assert.True(authorNavigation.IsOnDependent); - Assert.Equal("TestNamespace.Person", authorNavigation.ForeignKey.PrincipalEntityType.Name); - - var contributionsNav = postType.FindNavigation("Contributions"); - Assert.False(contributionsNav.IsOnDependent); - Assert.Equal("TestNamespace.Contribution", contributionsNav.ForeignKey.DeclaringEntityType.Name); - }); + Assert.False(model.FindEntityType("TestNamespace.Entity").GetProperty("RequiredString").IsNullable)); } [ConditionalFact] - public void Navigation_property_with_same_type_and_navigation_name() + public void ColumnAttribute_is_generated_for_property() { Test( modelBuilder => modelBuilder .Entity( - "Blog", - x => x.Property("Id")) - .Entity( - "Post", + "Entity", x => { x.Property("Id"); - x.HasOne("Blog", "Blog").WithMany("Posts"); + x.Property("A").HasColumnName("propertyA"); + x.Property("B").HasColumnType("nchar(10)"); + x.Property("C").HasColumnName("random").HasColumnType("varchar(200)"); + x.Property("D").HasColumnType("numeric(18, 2)"); + x.Property("E").HasMaxLength(100); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var postFile = code.AdditionalFiles.First(f => f.Path == "Post.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -106,54 +716,101 @@ public void Navigation_property_with_same_type_and_navigation_name() namespace TestNamespace { - public partial class Post + public partial class Entity { [Key] public int Id { get; set; } - public int? BlogId { get; set; } + [Column(""propertyA"")] + public string A { get; set; } + [Column(TypeName = ""nchar(10)"")] + public string B { get; set; } + [Column(""random"", TypeName = ""varchar(200)"")] + public string C { get; set; } + [Column(TypeName = ""numeric(18, 2)"")] + public decimal D { get; set; } + [StringLength(100)] + public string E { get; set; } + } +} +", + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); - [ForeignKey(nameof(BlogId))] - [InverseProperty(""Posts"")] - public virtual Blog Blog { get; set; } + 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 Entity { 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); } } ", - postFile.Code, ignoreLineEndingDifferences: true); + code.ContextFile); }, model => { - var postType = model.FindEntityType("TestNamespace.Post"); - var blogNavigation = postType.FindNavigation("Blog"); - - var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); - Assert.Equal("BlogId", foreignKeyProperty.Name); - - var inverseNavigation = blogNavigation.Inverse; - Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); - Assert.Equal("Posts", inverseNavigation.Name); + var entitType = model.FindEntityType("TestNamespace.Entity"); + Assert.Equal("propertyA", entitType.GetProperty("A").GetColumnName()); + Assert.Equal("nchar(10)", entitType.GetProperty("B").GetColumnType()); + Assert.Equal("random", entitType.GetProperty("C").GetColumnName()); + Assert.Equal("varchar(200)", entitType.GetProperty("C").GetColumnType()); }); } [ConditionalFact] - public void Navigation_property_with_same_type_and_property_name() + public void MaxLengthAttribute_is_generated_for_property() { Test( modelBuilder => modelBuilder .Entity( - "Blog", - x => x.Property("Id")) - .Entity( - "Post", + "Entity", x => { x.Property("Id"); - x.HasOne("Blog", "BlogNavigation").WithMany("Posts").HasForeignKey("Blog"); + x.Property("A").HasMaxLength(34); + x.Property("B").HasMaxLength(10); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var postFile = code.AdditionalFiles.First(f => f.Path == "Post.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -164,55 +821,45 @@ public void Navigation_property_with_same_type_and_property_name() namespace TestNamespace { - public partial class Post + public partial class Entity { [Key] public int Id { get; set; } - public int? Blog { get; set; } - - [ForeignKey(nameof(Blog))] - [InverseProperty(""Posts"")] - public virtual Blog BlogNavigation { get; set; } + [StringLength(34)] + public string A { get; set; } + [MaxLength(10)] + public byte[] B { get; set; } } } ", - postFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); }, model => { - var postType = model.FindEntityType("TestNamespace.Post"); - var blogNavigation = postType.FindNavigation("BlogNavigation"); - - var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); - Assert.Equal("Blog", foreignKeyProperty.Name); - - var inverseNavigation = blogNavigation.Inverse; - Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); - Assert.Equal("Posts", inverseNavigation.Name); + var entitType = model.FindEntityType("TestNamespace.Entity"); + Assert.Equal(34, entitType.GetProperty("A").GetMaxLength()); + Assert.Equal(10, entitType.GetProperty("B").GetMaxLength()); }); } [ConditionalFact] - public void Navigation_property_with_same_type_and_other_navigation_name() + public void Properties_are_sorted_in_order_of_definition_in_table() { Test( modelBuilder => modelBuilder .Entity( - "Blog", - x => x.Property("Id")) - .Entity( - "Post", + "Entity", x => { + // Order would be PK first and then rest alphabetically since they are all shadow x.Property("Id"); - x.HasOne("Blog", "Blog").WithMany("Posts"); - x.HasOne("Blog", "OriginalBlog").WithMany("OriginalPosts").HasForeignKey("OriginalBlogId"); + x.Property("LastProperty"); + x.Property("FirstProperty"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var postFile = code.AdditionalFiles.First(f => f.Path == "Post.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -223,66 +870,44 @@ public void Navigation_property_with_same_type_and_other_navigation_name() namespace TestNamespace { - public partial class Post + public partial class Entity { [Key] public int Id { get; set; } - public int? BlogId { get; set; } - public int? OriginalBlogId { get; set; } - - [ForeignKey(nameof(BlogId))] - [InverseProperty(""Posts"")] - public virtual Blog Blog { get; set; } - [ForeignKey(nameof(OriginalBlogId))] - [InverseProperty(""OriginalPosts"")] - public virtual Blog OriginalBlog { get; set; } + public string FirstProperty { get; set; } + public string LastProperty { get; set; } } } ", - postFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); }, - model => - { - var postType = model.FindEntityType("TestNamespace.Post"); - - var blogNavigation = postType.FindNavigation("Blog"); - - var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); - Assert.Equal("BlogId", foreignKeyProperty.Name); - - var inverseNavigation = blogNavigation.Inverse; - Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); - Assert.Equal("Posts", inverseNavigation.Name); - - var originalBlogNavigation = postType.FindNavigation("OriginalBlog"); - - var originalForeignKeyProperty = Assert.Single(originalBlogNavigation.ForeignKey.Properties); - Assert.Equal("OriginalBlogId", originalForeignKeyProperty.Name); - - var originalInverseNavigation = originalBlogNavigation.Inverse; - Assert.Equal("TestNamespace.Blog", originalInverseNavigation.DeclaringEntityType.Name); - Assert.Equal("OriginalPosts", originalInverseNavigation.Name); - }); + model => { }); } [ConditionalFact] - public void Composite_key() + public void Navigation_properties_are_sorted_after_properties_and_collection_are_initialized_in_ctor() { Test( modelBuilder => modelBuilder + .Entity( + "Person", + x => x.Property("Id")) + .Entity( + "Contribution", + x => x.Property("Id")) .Entity( "Post", x => { - x.Property("Key"); - x.Property("Serial"); - x.HasKey("Key", "Serial"); + x.Property("Id"); + + x.HasOne("Person", "Author").WithMany("Posts"); + x.HasMany("Contribution", "Contributions").WithOne(); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var postFile = code.AdditionalFiles.First(f => f.Path == "Post.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -295,32 +920,25 @@ namespace TestNamespace { public partial class Post { + public Post() + { + Contributions = new HashSet(); + } + [Key] - public int Key { get; set; } - [Key] - public int Serial { get; set; } + public int Id { get; set; } + public int? AuthorId { get; set; } + + [ForeignKey(nameof(AuthorId))] + [InverseProperty(nameof(Person.Posts))] + public virtual Person Author { get; set; } + public virtual ICollection Contributions { get; set; } } } ", - postFile.Code, ignoreLineEndingDifferences: true); - }, - model => - { - var postType = model.FindEntityType("TestNamespace.Post"); - Assert.NotNull(postType.FindPrimaryKey()); - }); - } + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); - [ConditionalFact] - public void Views_dont_generate_TableAttribute() - { - Test( - modelBuilder => modelBuilder.Entity("Vista").ToView("Vistas", "dbo"), - new ModelCodeGenerationOptions { UseDataAnnotations = true }, - code => - { - var vistaFile = code.AdditionalFiles.First(f => f.Path == "Vista.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -331,34 +949,61 @@ public void Views_dont_generate_TableAttribute() namespace TestNamespace { - [Keyless] - public partial class Vista + public partial class Person { + public Person() + { + Posts = new HashSet(); + } + + [Key] + public int Id { get; set; } + + [InverseProperty(nameof(Post.Author))] + public virtual ICollection Posts { get; set; } } } ", - vistaFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Person.cs")); }, model => { - var entityType = model.FindEntityType("TestNamespace.Vista"); - Assert.Equal("Vistas", entityType.GetViewName()); - Assert.Null(entityType.GetTableName()); - Assert.Equal("dbo", entityType.GetViewSchema()); - Assert.Null(entityType.GetSchema()); + var postType = model.FindEntityType("TestNamespace.Post"); + var authorNavigation = postType.FindNavigation("Author"); + Assert.True(authorNavigation.IsOnDependent); + Assert.Equal("TestNamespace.Person", authorNavigation.ForeignKey.PrincipalEntityType.Name); + + var contributionsNav = postType.FindNavigation("Contributions"); + Assert.False(contributionsNav.IsOnDependent); + Assert.Equal("TestNamespace.Contribution", contributionsNav.ForeignKey.DeclaringEntityType.Name); }); } [ConditionalFact] - public void Keyless_entity_generates_KeylesssAttribute() + public void ForeignKeyAttribute_is_generated_for_composite_fk() { Test( - modelBuilder => modelBuilder.Entity("Vista").HasNoKey(), + modelBuilder => modelBuilder + .Entity( + "Blog", + x => + { + x.Property("Id1"); + x.Property("Id2"); + x.HasKey("Id1", "Id2"); + }) + .Entity( + "Post", + x => + { + x.Property("Id"); + + x.HasOne("Blog", "BlogNavigation").WithMany("Posts").HasForeignKey("BlogId1", "BlogId2"); + }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var vistaFile = code.AdditionalFiles.First(f => f.Path == "Vista.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -369,15 +1014,22 @@ public void Keyless_entity_generates_KeylesssAttribute() namespace TestNamespace { - [Keyless] - public partial class Vista + public partial class Post { + [Key] + public int Id { get; set; } + public int? BlogId1 { get; set; } + public int? BlogId2 { get; set; } + + [ForeignKey(""BlogId1,BlogId2"")] + [InverseProperty(nameof(Blog.Posts))] + public virtual Blog BlogNavigation { get; set; } } } ", - vistaFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); - Assert.Equal( + AssertFileContents( @"using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; @@ -397,19 +1049,32 @@ public TestDbContext(DbContextOptions options) { } - public virtual DbSet Vista { get; set; } + public virtual DbSet Blog { get; set; } + public virtual DbSet Post { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { -#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. +#warning " + + DesignStrings.SensitiveInformationWarning + + @" optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase""); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity(entity => + { + entity.HasKey(e => new { e.Id1, e.Id2 }); + }); + + modelBuilder.Entity(entity => + { + entity.Property(e => e.Id).UseIdentityColumn(); + }); + OnModelCreatingPartial(modelBuilder); } @@ -417,38 +1082,44 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } ", - code.ContextFile.Code, ignoreLineEndingDifferences: true); + code.ContextFile); }, model => { - var entityType = model.FindEntityType("TestNamespace.Vista"); - Assert.Null(entityType.FindPrimaryKey()); + var postType = model.FindEntityType("TestNamespace.Post"); + var blogNavigation = postType.FindNavigation("BlogNavigation"); + Assert.Equal("TestNamespace.Blog", blogNavigation.ForeignKey.PrincipalEntityType.Name); + Assert.Equal(new[] { "BlogId1", "BlogId2" }, blogNavigation.ForeignKey.Properties.Select(p => p.Name)); }); } [ConditionalFact] - public void Entity_with_multiple_indexes_generates_multiple_IndexAttributes() + public void ForeignKeyAttribute_InversePropertyAttribute_is_not_generated_for_alternate_key() { Test( modelBuilder => modelBuilder .Entity( - "EntityWithIndexes", + "Blog", x => { x.Property("Id"); - x.Property("A"); - x.Property("B"); - x.Property("C"); - x.HasKey("Id"); - x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB") - .IsUnique(); - x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC"); + x.Property("Id1"); + x.Property("Id2"); + }) + .Entity( + "Post", + x => + { + x.Property("Id"); + + x.HasOne("Blog", "BlogNavigation").WithMany("Posts") + .HasPrincipalKey("Id1", "Id2") + .HasForeignKey("BlogId1", "BlogId2"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var entityFile = code.AdditionalFiles.First(f => f.Path == "EntityWithIndexes.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -459,138 +1130,108 @@ public void Entity_with_multiple_indexes_generates_multiple_IndexAttributes() namespace TestNamespace { - [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] - [Index(nameof(B), nameof(C), Name = ""IndexOnBAndC"")] - public partial class EntityWithIndexes + public partial class Post { [Key] public int Id { get; set; } - public int A { get; set; } - public int B { get; set; } - public int C { get; set; } + public int? BlogId1 { get; set; } + public int? BlogId2 { get; set; } + + public virtual Blog BlogNavigation { get; set; } } } ", - entityFile.Code, ignoreLineEndingDifferences: true); - }, - model => - { - var entityType = model.FindEntityType("TestNamespace.EntityWithIndexes"); - var indexes = entityType.GetIndexes(); - Assert.Equal(2, indexes.Count()); - Assert.Equal("IndexOnAAndB", indexes.First().Name); - Assert.Equal("IndexOnBAndC", indexes.Skip(1).First().Name); - }); - } + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); - [ConditionalFact] - public void Entity_with_indexes_generates_IndexAttribute_only_for_indexes_without_annotations() - { - Test( - modelBuilder => modelBuilder - .Entity( - "EntityWithIndexes", - x => - { - x.Property("Id"); - x.Property("A"); - x.Property("B"); - x.Property("C"); - x.HasKey("Id"); - x.HasIndex(new[] { "A", "B" }, "IndexOnAAndB") - .IsUnique(); - x.HasIndex(new[] { "B", "C" }, "IndexOnBAndC") - .HasFilter("Filter SQL"); - }), - new ModelCodeGenerationOptions { UseDataAnnotations = true }, - code => - { - var entityFile = code.AdditionalFiles.First(f => f.Path == "EntityWithIndexes.cs"); - Assert.Equal( + AssertFileContents( @"using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; #nullable disable namespace TestNamespace { - [Index(nameof(A), nameof(B), Name = ""IndexOnAAndB"", IsUnique = true)] - public partial class EntityWithIndexes + public partial class TestDbContext : DbContext { - [Key] - public int Id { get; set; } - public int A { get; set; } - public int B { get; set; } - public int C { get; set; } - } -} -", - entityFile.Code, ignoreLineEndingDifferences: true); - }, - model => - Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + public TestDbContext() + { } - [ConditionalFact] - public void KeyAttribute_is_generated_for_property() + public TestDbContext(DbContextOptions options) + : base(options) { - Test( - modelBuilder => modelBuilder - .Entity( - "Entity", - x => - { - x.Property("PrimaryKey"); - x.HasKey("PrimaryKey"); - }), - new ModelCodeGenerationOptions { UseDataAnnotations = true }, - code => - { - var entityFile = code.AdditionalFiles.First(f => f.Path == "Entity.cs"); - Assert.Equal( - @"using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore; + } -#nullable disable + public virtual DbSet Blog { get; set; } + public virtual DbSet Post { get; set; } -namespace TestNamespace -{ - public partial class Entity - { - [Key] - public int PrimaryKey { 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(); + }); + + modelBuilder.Entity(entity => + { + entity.Property(e => e.Id).UseIdentityColumn(); + + entity.HasOne(d => d.BlogNavigation) + .WithMany(p => p.Posts) + .HasPrincipalKey(p => new { p.Id1, p.Id2 }) + .HasForeignKey(d => new { d.BlogId1, d.BlogId2 }); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); } } ", - entityFile.Code, ignoreLineEndingDifferences: true); + code.ContextFile); }, model => - Assert.Equal("PrimaryKey", model.FindEntityType("TestNamespace.Entity").FindPrimaryKey().Properties[0].Name)); + { + var postType = model.FindEntityType("TestNamespace.Post"); + var blogNavigation = postType.FindNavigation("BlogNavigation"); + Assert.Equal("TestNamespace.Blog", blogNavigation.ForeignKey.PrincipalEntityType.Name); + Assert.Equal(new[] { "BlogId1", "BlogId2" }, blogNavigation.ForeignKey.Properties.Select(p => p.Name)); + Assert.Equal(new[] { "Id1", "Id2" }, blogNavigation.ForeignKey.PrincipalKey.Properties.Select(p => p.Name)); + }); } [ConditionalFact] - public void RequiredAttribute_is_generated_for_property() + public void InverseProperty_when_navigation_property_with_same_type_and_navigation_name() { Test( modelBuilder => modelBuilder .Entity( - "Entity", + "Blog", + x => x.Property("Id")) + .Entity( + "Post", x => { x.Property("Id"); - x.Property("RequiredString").IsRequired(); + x.HasOne("Blog", "Blog").WithMany("Posts"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var entityFile = code.AdditionalFiles.First(f => f.Path == "Entity.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -601,40 +1242,53 @@ public void RequiredAttribute_is_generated_for_property() namespace TestNamespace { - public partial class Entity + public partial class Post { [Key] public int Id { get; set; } - [Required] - public string RequiredString { get; set; } + public int? BlogId { get; set; } + + [ForeignKey(nameof(BlogId))] + [InverseProperty(""Posts"")] + public virtual Blog Blog { get; set; } } } ", - entityFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); }, model => - Assert.False(model.FindEntityType("TestNamespace.Entity").GetProperty("RequiredString").IsNullable)); + { + var postType = model.FindEntityType("TestNamespace.Post"); + var blogNavigation = postType.FindNavigation("Blog"); + + var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); + Assert.Equal("BlogId", foreignKeyProperty.Name); + + var inverseNavigation = blogNavigation.Inverse; + Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); + Assert.Equal("Posts", inverseNavigation.Name); + }); } [ConditionalFact] - public void ColumnAttribute_is_generated_for_property() + public void InverseProperty_when_navigation_property_with_same_type_and_property_name() { Test( modelBuilder => modelBuilder .Entity( - "Entity", + "Blog", + x => x.Property("Id")) + .Entity( + "Post", x => { x.Property("Id"); - x.Property("A").HasColumnName("propertyA"); - x.Property("B").HasColumnType("nchar(10)"); - x.Property("C").HasColumnName("random").HasColumnType("varchar(200)"); + x.HasOne("Blog", "BlogNavigation").WithMany("Posts").HasForeignKey("Blog"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var entityFile = code.AdditionalFiles.First(f => f.Path == "Entity.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -645,49 +1299,54 @@ public void ColumnAttribute_is_generated_for_property() namespace TestNamespace { - public partial class Entity + public partial class Post { [Key] public int Id { get; set; } - [Column(""propertyA"")] - public string A { get; set; } - [Column(TypeName = ""nchar(10)"")] - public string B { get; set; } - [Column(""random"", TypeName = ""varchar(200)"")] - public string C { get; set; } + public int? Blog { get; set; } + + [ForeignKey(nameof(Blog))] + [InverseProperty(""Posts"")] + public virtual Blog BlogNavigation { get; set; } } } ", - entityFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); }, model => { - var entitType = model.FindEntityType("TestNamespace.Entity"); - Assert.Equal("propertyA", entitType.GetProperty("A").GetColumnName()); - Assert.Equal("nchar(10)", entitType.GetProperty("B").GetColumnType()); - Assert.Equal("random", entitType.GetProperty("C").GetColumnName()); - Assert.Equal("varchar(200)", entitType.GetProperty("C").GetColumnType()); + var postType = model.FindEntityType("TestNamespace.Post"); + var blogNavigation = postType.FindNavigation("BlogNavigation"); + + var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); + Assert.Equal("Blog", foreignKeyProperty.Name); + + var inverseNavigation = blogNavigation.Inverse; + Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); + Assert.Equal("Posts", inverseNavigation.Name); }); } [ConditionalFact] - public void MaxLengthAttribute_is_generated_for_property() + public void InverseProperty_when_navigation_property_with_same_type_and_other_navigation_name() { Test( modelBuilder => modelBuilder .Entity( - "Entity", + "Blog", + x => x.Property("Id")) + .Entity( + "Post", x => { x.Property("Id"); - x.Property("A").HasMaxLength(34); - x.Property("B").HasMaxLength(10); + x.HasOne("Blog", "Blog").WithMany("Posts"); + x.HasOne("Blog", "OriginalBlog").WithMany("OriginalPosts").HasForeignKey("OriginalBlogId"); }), new ModelCodeGenerationOptions { UseDataAnnotations = true }, code => { - var entityFile = code.AdditionalFiles.First(f => f.Path == "Entity.cs"); - Assert.Equal( + AssertFileContents( @"using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -698,25 +1357,48 @@ public void MaxLengthAttribute_is_generated_for_property() namespace TestNamespace { - public partial class Entity + public partial class Post { [Key] public int Id { get; set; } - [StringLength(34)] - public string A { get; set; } - [MaxLength(10)] - public byte[] B { get; set; } + public int? BlogId { get; set; } + public int? OriginalBlogId { get; set; } + + [ForeignKey(nameof(BlogId))] + [InverseProperty(""Posts"")] + public virtual Blog Blog { get; set; } + [ForeignKey(nameof(OriginalBlogId))] + [InverseProperty(""OriginalPosts"")] + public virtual Blog OriginalBlog { get; set; } } } ", - entityFile.Code, ignoreLineEndingDifferences: true); + code.AdditionalFiles.Single(f => f.Path == "Post.cs")); }, model => { - var entitType = model.FindEntityType("TestNamespace.Entity"); - Assert.Equal(34, entitType.GetProperty("A").GetMaxLength()); - Assert.Equal(10, entitType.GetProperty("B").GetMaxLength()); + var postType = model.FindEntityType("TestNamespace.Post"); + + var blogNavigation = postType.FindNavigation("Blog"); + + var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties); + Assert.Equal("BlogId", foreignKeyProperty.Name); + + var inverseNavigation = blogNavigation.Inverse; + Assert.Equal("TestNamespace.Blog", inverseNavigation.DeclaringEntityType.Name); + Assert.Equal("Posts", inverseNavigation.Name); + + var originalBlogNavigation = postType.FindNavigation("OriginalBlog"); + + var originalForeignKeyProperty = Assert.Single(originalBlogNavigation.ForeignKey.Properties); + Assert.Equal("OriginalBlogId", originalForeignKeyProperty.Name); + + var originalInverseNavigation = originalBlogNavigation.Inverse; + Assert.Equal("TestNamespace.Blog", originalInverseNavigation.DeclaringEntityType.Name); + Assert.Equal("OriginalPosts", originalInverseNavigation.Name); }); } + + } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index c891cd7ab6b..213627ea0f8 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -10,19 +10,12 @@ using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; +using Xunit; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal { public abstract class ModelCodeGeneratorTestBase { - protected void Test( - Action buildModel, - ModelCodeGenerationOptions options, - Action assertScaffold) - { - Test(buildModel, options, assertScaffold, null); - } - protected void Test( Action buildModel, ModelCodeGenerationOptions options, @@ -76,5 +69,10 @@ protected void Test( assertModel(compiledModel); } } + + protected static void AssertFileContents( + string expectedCode, + ScaffoldedFile file) + => Assert.Equal(expectedCode, file.Code, ignoreLineEndingDifferences: true); } }