diff --git a/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs b/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs index 9baa1c357bb..5d1466d1da1 100644 --- a/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs +++ b/src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs @@ -202,6 +202,11 @@ public static IEnumerable GetDataAnnotations( yield return new AttributeCodeFragment(typeof(RequiredAttribute)); } + if (IsRowVersion(property)) + { + yield return new AttributeCodeFragment(typeof(TimestampAttribute)); + } + var columnName = property.GetColumnName(); if (columnName == property.Name) { @@ -634,6 +639,8 @@ public static IEnumerable GetDataAnnotations( root = root?.Chain(isUnicode) ?? isUnicode; } + var isRowVersion = IsRowVersion(property); + var valueGenerated = property.ValueGenerated; if (((IConventionProperty)property).GetValueGeneratedConfigurationSource() is { } valueGeneratedConfigurationSource && valueGeneratedConfigurationSource != ConfigurationSource.Convention @@ -649,14 +656,20 @@ public static IEnumerable GetDataAnnotations( ValueGenerated.OnUpdate => nameof(PropertyBuilder.ValueGeneratedOnUpdate), ValueGenerated.Never => nameof(PropertyBuilder.ValueGeneratedNever), _ => throw new InvalidOperationException(DesignStrings.UnhandledEnumValue($"{nameof(ValueGenerated)}.{valueGenerated}")) - }); + }) + { + IsHandledByDataAnnotations = isRowVersion + }; root = root?.Chain(valueGeneratedCall) ?? valueGeneratedCall; } if (property.IsConcurrencyToken) { - var isConcurrencyToken = new FluentApiCodeFragment(nameof(PropertyBuilder.IsConcurrencyToken)); + var isConcurrencyToken = new FluentApiCodeFragment(nameof(PropertyBuilder.IsConcurrencyToken)) + { + IsHandledByDataAnnotations = isRowVersion + }; root = root?.Chain(isConcurrencyToken) ?? isConcurrencyToken; } @@ -817,6 +830,10 @@ public static IEnumerable GetDataAnnotations( return root; } + private static bool IsRowVersion(IProperty property) + => property.IsConcurrencyToken + && property.ValueGenerated == ValueGenerated.OnAddOrUpdate; + private static FluentApiCodeFragment? GenerateAnnotations( IAnnotatable annotatable, Dictionary annotations, diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index a52ed3603da..e9f35fc1c03 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -395,6 +395,48 @@ public Task ValueGenerated_works() Assert.Equal(ValueGenerated.Never, entity.GetProperty("ValueGeneratedNever").ValueGenerated); }); + [Fact] + public Task IsRowVersion_is_still_generated_when_not_using_data_annotations() + => TestAsync( + modelBuilder => modelBuilder.Entity( + "Entity", + x => + { + x.Property("Id"); + x.Property("Version").IsRowVersion(); + }), + new ModelCodeGenerationOptions(), + code => Assert.Contains("IsRowVersion()", code.ContextFile.Code), + model => + { + var property = model.FindEntityType("TestNamespace.Entity").GetProperty("Version"); + Assert.True(property.IsConcurrencyToken); + Assert.Equal(ValueGenerated.OnAddOrUpdate, property.ValueGenerated); + }); + + [Fact] + public Task IsConcurrencyToken_is_still_generated_for_non_row_version_property_using_data_annotations() + => TestAsync( + modelBuilder => modelBuilder.Entity( + "Entity", + x => + { + x.Property("Id"); + x.Property("Token").IsConcurrencyToken(); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + Assert.Contains("IsConcurrencyToken()", code.ContextFile.Code); + Assert.DoesNotContain("[Timestamp]", code.AdditionalFiles.Single(f => f.Path == "Entity.cs").Code); + }, + model => + { + var property = model.FindEntityType("TestNamespace.Entity").GetProperty("Token"); + Assert.True(property.IsConcurrencyToken); + Assert.NotEqual(ValueGenerated.OnAddOrUpdate, property.ValueGenerated); + }); + [Fact] public Task HasPrecision_works() => TestAsync( diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs index 4c407e688d3..fe4afc7074e 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs @@ -1197,6 +1197,51 @@ public partial class Entity Assert.Equal(3, entitType.GetProperty("D").GetPrecision()); }); + [Fact] + public Task TimestampAttribute_is_generated_for_row_version_property() + => TestAsync( + modelBuilder => modelBuilder + .Entity( + "Entity", + x => + { + x.Property("Id"); + x.Property("Version").IsRowVersion(); + }), + new ModelCodeGenerationOptions { UseDataAnnotations = true }, + code => + { + AssertFileContents( + """ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace TestNamespace; + +public partial class Entity +{ + [Key] + public int Id { get; set; } + + [Timestamp] + public byte[] Version { get; set; } +} +""", + code.AdditionalFiles.Single(f => f.Path == "Entity.cs")); + + Assert.DoesNotContain("IsRowVersion", code.ContextFile.Code); + Assert.DoesNotContain("IsConcurrencyToken", code.ContextFile.Code); + }, + model => + { + var property = model.FindEntityType("TestNamespace.Entity").GetProperty("Version"); + Assert.True(property.IsConcurrencyToken); + Assert.Equal(ValueGenerated.OnAddOrUpdate, property.ValueGenerated); + }); + [Fact] public Task Comments_are_generated() => TestAsync(