Skip to content

Commit 0e28884

Browse files
Fix missing code generation output for custom data annotation attributes (#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>
1 parent 595974e commit 0e28884

4 files changed

Lines changed: 318 additions & 12 deletions

File tree

src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ protected virtual void GenerateEntityTypeDataAnnotations([NotNull] IEntityType e
151151
{
152152
attributeWriter.AddParameter(_code.UnknownLiteral(argument));
153153
}
154+
155+
_sb.AppendLine(attributeWriter.ToString());
154156
}
155157
}
156158

@@ -300,6 +302,8 @@ protected virtual void GeneratePropertyDataAnnotations([NotNull] IProperty prope
300302
{
301303
attributeWriter.AddParameter(_code.UnknownLiteral(argument));
302304
}
305+
306+
_sb.AppendLine(attributeWriter.ToString());
303307
}
304308
}
305309

test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Collections.Generic;
46
using System.Linq;
7+
using Microsoft.EntityFrameworkCore.Design;
8+
using Microsoft.EntityFrameworkCore.Infrastructure;
59
using Microsoft.EntityFrameworkCore.Internal;
10+
using Microsoft.EntityFrameworkCore.Metadata;
11+
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
12+
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.DependencyInjection.Extensions;
615
using Xunit;
716

817
namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal
@@ -1402,5 +1411,277 @@ public partial class Post
14021411
Assert.Equal("OriginalPosts", originalInverseNavigation.Name);
14031412
});
14041413
}
1414+
1415+
[ConditionalFact]
1416+
public void Entity_with_custom_annotation()
1417+
{
1418+
Test(
1419+
modelBuilder => modelBuilder
1420+
.Entity(
1421+
"EntityWithAnnotation",
1422+
x =>
1423+
{
1424+
x.HasAnnotation("Custom:EntityAnnotation", "first argument");
1425+
x.Property<int>("Id");
1426+
x.HasKey("Id");
1427+
}),
1428+
new ModelCodeGenerationOptions { UseDataAnnotations = true },
1429+
code =>
1430+
{
1431+
AssertFileContents(
1432+
@"using System;
1433+
using System.Collections.Generic;
1434+
using System.ComponentModel.DataAnnotations;
1435+
using System.ComponentModel.DataAnnotations.Schema;
1436+
using Microsoft.EntityFrameworkCore;
1437+
1438+
#nullable disable
1439+
1440+
namespace TestNamespace
1441+
{
1442+
[CustomEntityDataAnnotation(""first argument"")]
1443+
public partial class EntityWithAnnotation
1444+
{
1445+
[Key]
1446+
public int Id { get; set; }
1447+
}
1448+
}
1449+
",
1450+
code.AdditionalFiles.Single(f => f.Path == "EntityWithAnnotation.cs"));
1451+
1452+
AssertFileContents(
1453+
@"using System;
1454+
using Microsoft.EntityFrameworkCore;
1455+
using Microsoft.EntityFrameworkCore.Metadata;
1456+
1457+
#nullable disable
1458+
1459+
namespace TestNamespace
1460+
{
1461+
public partial class TestDbContext : DbContext
1462+
{
1463+
public TestDbContext()
1464+
{
1465+
}
1466+
1467+
public TestDbContext(DbContextOptions<TestDbContext> options)
1468+
: base(options)
1469+
{
1470+
}
1471+
1472+
public virtual DbSet<EntityWithAnnotation> EntityWithAnnotation { get; set; }
1473+
1474+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
1475+
{
1476+
if (!optionsBuilder.IsConfigured)
1477+
{
1478+
#warning "
1479+
+ DesignStrings.SensitiveInformationWarning
1480+
+ @"
1481+
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
1482+
}
1483+
}
1484+
1485+
protected override void OnModelCreating(ModelBuilder modelBuilder)
1486+
{
1487+
modelBuilder.Entity<EntityWithAnnotation>(entity =>
1488+
{
1489+
entity.Property(e => e.Id).UseIdentityColumn();
1490+
});
1491+
1492+
OnModelCreatingPartial(modelBuilder);
1493+
}
1494+
1495+
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
1496+
}
1497+
}
1498+
",
1499+
code.ContextFile);
1500+
},
1501+
assertModel: null,
1502+
skipBuild: true);
1503+
}
1504+
1505+
[ConditionalFact]
1506+
public void Entity_property_with_custom_annotation()
1507+
{
1508+
Test(
1509+
modelBuilder => modelBuilder
1510+
.Entity(
1511+
"EntityWithPropertyAnnotation",
1512+
x =>
1513+
{
1514+
x.Property<int>("Id")
1515+
.HasAnnotation("Custom:PropertyAnnotation", "first argument");
1516+
x.HasKey("Id");
1517+
}),
1518+
new ModelCodeGenerationOptions { UseDataAnnotations = true },
1519+
code =>
1520+
{
1521+
AssertFileContents(
1522+
@"using System;
1523+
using System.Collections.Generic;
1524+
using System.ComponentModel.DataAnnotations;
1525+
using System.ComponentModel.DataAnnotations.Schema;
1526+
using Microsoft.EntityFrameworkCore;
1527+
1528+
#nullable disable
1529+
1530+
namespace TestNamespace
1531+
{
1532+
public partial class EntityWithPropertyAnnotation
1533+
{
1534+
[Key]
1535+
[CustomPropertyDataAnnotation(""first argument"")]
1536+
public int Id { get; set; }
1537+
}
1538+
}
1539+
",
1540+
code.AdditionalFiles.Single(f => f.Path == "EntityWithPropertyAnnotation.cs"));
1541+
1542+
AssertFileContents(
1543+
@"using System;
1544+
using Microsoft.EntityFrameworkCore;
1545+
using Microsoft.EntityFrameworkCore.Metadata;
1546+
1547+
#nullable disable
1548+
1549+
namespace TestNamespace
1550+
{
1551+
public partial class TestDbContext : DbContext
1552+
{
1553+
public TestDbContext()
1554+
{
1555+
}
1556+
1557+
public TestDbContext(DbContextOptions<TestDbContext> options)
1558+
: base(options)
1559+
{
1560+
}
1561+
1562+
public virtual DbSet<EntityWithPropertyAnnotation> EntityWithPropertyAnnotation { get; set; }
1563+
1564+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
1565+
{
1566+
if (!optionsBuilder.IsConfigured)
1567+
{
1568+
#warning "
1569+
+ DesignStrings.SensitiveInformationWarning
1570+
+ @"
1571+
optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");
1572+
}
1573+
}
1574+
1575+
protected override void OnModelCreating(ModelBuilder modelBuilder)
1576+
{
1577+
modelBuilder.Entity<EntityWithPropertyAnnotation>(entity =>
1578+
{
1579+
entity.Property(e => e.Id).UseIdentityColumn();
1580+
});
1581+
1582+
OnModelCreatingPartial(modelBuilder);
1583+
}
1584+
1585+
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
1586+
}
1587+
}
1588+
",
1589+
code.ContextFile);
1590+
},
1591+
assertModel: null,
1592+
skipBuild: true);
1593+
}
1594+
1595+
protected override void AddModelServices(IServiceCollection services)
1596+
{
1597+
services.Replace(ServiceDescriptor.Singleton<IRelationalAnnotationProvider, ModelAnnotationProvider>());
1598+
}
1599+
1600+
protected override void AddScaffoldingServices(IServiceCollection services)
1601+
{
1602+
services.Replace(ServiceDescriptor.Singleton<IAnnotationCodeGenerator, ModelAnnotationCodeGenerator>());
1603+
}
1604+
1605+
public class ModelAnnotationProvider : SqlServerAnnotationProvider
1606+
{
1607+
public ModelAnnotationProvider(RelationalAnnotationProviderDependencies dependencies)
1608+
: base(dependencies)
1609+
{
1610+
}
1611+
1612+
/// <inheritdoc />
1613+
public override IEnumerable<IAnnotation> For(ITable table)
1614+
{
1615+
foreach (var annotation in base.For(table))
1616+
{
1617+
yield return annotation;
1618+
}
1619+
1620+
var entityType = table.EntityTypeMappings.First().EntityType;
1621+
1622+
foreach (var annotation in entityType.GetAnnotations().Where(a => a.Name == "Custom:EntityAnnotation"))
1623+
{
1624+
yield return annotation;
1625+
}
1626+
}
1627+
1628+
/// <inheritdoc />
1629+
public override IEnumerable<IAnnotation> For(IColumn column)
1630+
{
1631+
foreach (var annotation in base.For(column))
1632+
{
1633+
yield return annotation;
1634+
}
1635+
1636+
var properties = column.PropertyMappings.Select(m => m.Property);
1637+
var annotations = properties.SelectMany(p => p.GetAnnotations()).GroupBy(a => a.Name).Select(g => g.First());
1638+
1639+
foreach (var annotation in annotations.Where(a => a.Name == "Custom:PropertyAnnotation"))
1640+
{
1641+
yield return annotation;
1642+
}
1643+
}
1644+
}
1645+
1646+
public class ModelAnnotationCodeGenerator : SqlServerAnnotationCodeGenerator
1647+
{
1648+
public ModelAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies)
1649+
: base(dependencies)
1650+
{
1651+
}
1652+
1653+
protected override AttributeCodeFragment GenerateDataAnnotation(IEntityType entityType, IAnnotation annotation)
1654+
=> annotation.Name switch
1655+
{
1656+
"Custom:EntityAnnotation" => new AttributeCodeFragment(
1657+
typeof(CustomEntityDataAnnotationAttribute), new object[] { annotation.Value as string }),
1658+
_ => base.GenerateDataAnnotation(entityType, annotation)
1659+
};
1660+
1661+
protected override AttributeCodeFragment GenerateDataAnnotation(IProperty property, IAnnotation annotation)
1662+
=> annotation.Name switch
1663+
{
1664+
"Custom:PropertyAnnotation" => new AttributeCodeFragment(typeof(CustomPropertyDataAnnotationAttribute), new object[] {annotation.Value as string}),
1665+
_ => base.GenerateDataAnnotation(property, annotation)
1666+
};
1667+
}
1668+
1669+
[AttributeUsage(AttributeTargets.Class)]
1670+
public class CustomEntityDataAnnotationAttribute : Attribute
1671+
{
1672+
public CustomEntityDataAnnotationAttribute(string argument)
1673+
=> Argument = argument;
1674+
1675+
public virtual string Argument { get; }
1676+
}
1677+
1678+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
1679+
public class CustomPropertyDataAnnotationAttribute : Attribute
1680+
{
1681+
public CustomPropertyDataAnnotationAttribute(string argument)
1682+
=> Argument = argument;
1683+
1684+
public virtual string Argument { get; }
1685+
}
14051686
}
14061687
}

test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,24 @@ protected void Test(
2020
Action<ModelBuilder> buildModel,
2121
ModelCodeGenerationOptions options,
2222
Action<ScaffoldedModel> assertScaffold,
23-
Action<IModel> assertModel)
23+
Action<IModel> assertModel,
24+
bool skipBuild = false)
2425
{
25-
var modelBuilder = SqlServerTestHelpers.Instance.CreateConventionBuilder(skipValidation: true);
26+
var designServices = new ServiceCollection();
27+
AddModelServices(designServices);
28+
29+
var modelBuilder = SqlServerTestHelpers.Instance.CreateConventionBuilder(skipValidation: true, customServices: designServices);
2630
modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion);
2731
buildModel(modelBuilder);
2832
var _ = modelBuilder.Model.GetEntityTypeErrors();
2933

3034
var model = modelBuilder.FinalizeModel();
3135

32-
var services = new ServiceCollection()
33-
.AddEntityFrameworkDesignTimeServices();
36+
var services = new ServiceCollection();
37+
38+
services.AddEntityFrameworkDesignTimeServices();
3439
new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services);
40+
AddScaffoldingServices(services);
3541

3642
var generator = services
3743
.BuildServiceProvider()
@@ -60,16 +66,27 @@ protected void Test(
6066
scaffoldedModel.AdditionalFiles.Select(f => f.Code)))
6167
};
6268

63-
var assembly = build.BuildInMemory();
64-
var context = (DbContext)assembly.CreateInstance("TestNamespace.TestDbContext");
65-
66-
if (assertModel != null)
69+
if (!skipBuild)
6770
{
68-
var compiledModel = context.Model;
69-
assertModel(compiledModel);
71+
var assembly = build.BuildInMemory();
72+
var context = (DbContext)assembly.CreateInstance("TestNamespace.TestDbContext");
73+
74+
if (assertModel != null)
75+
{
76+
var compiledModel = context.Model;
77+
assertModel(compiledModel);
78+
}
7079
}
7180
}
7281

82+
protected virtual void AddModelServices(IServiceCollection services)
83+
{
84+
}
85+
86+
protected virtual void AddScaffoldingServices(IServiceCollection services)
87+
{
88+
}
89+
7390
protected static void AssertFileContents(
7491
string expectedCode,
7592
ScaffoldedFile file)

test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,13 @@ public IMutableModel BuildModelFor<TEntity>()
135135
return builder.Model;
136136
}
137137

138-
public ModelBuilder CreateConventionBuilder(bool skipValidation = false)
138+
public ModelBuilder CreateConventionBuilder(
139+
bool skipValidation = false,
140+
IServiceCollection customServices = null)
139141
{
140-
var conventionSet = CreateConventionSetBuilder().CreateConventionSet();
142+
customServices ??= new ServiceCollection();
143+
var contextServices = CreateContextServices(customServices);
144+
var conventionSet = contextServices.GetRequiredService<IConventionSetBuilder>().CreateConventionSet();
141145

142146
if (skipValidation)
143147
{

0 commit comments

Comments
 (0)