Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ public static IEnumerable<AttributeCodeFragment> GetDataAnnotations(
yield return new AttributeCodeFragment(typeof(RequiredAttribute));
}

if (IsRowVersion(property))
{
yield return new AttributeCodeFragment(typeof(TimestampAttribute));
}

var columnName = property.GetColumnName();
if (columnName == property.Name)
{
Expand Down Expand Up @@ -634,6 +639,8 @@ public static IEnumerable<AttributeCodeFragment> GetDataAnnotations(
root = root?.Chain(isUnicode) ?? isUnicode;
}

var isRowVersion = IsRowVersion(property);

var valueGenerated = property.ValueGenerated;
if (((IConventionProperty)property).GetValueGeneratedConfigurationSource() is { } valueGeneratedConfigurationSource
&& valueGeneratedConfigurationSource != ConfigurationSource.Convention
Expand All @@ -649,14 +656,20 @@ public static IEnumerable<AttributeCodeFragment> 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;
}
Expand Down Expand Up @@ -817,6 +830,10 @@ public static IEnumerable<AttributeCodeFragment> GetDataAnnotations(
return root;
}

private static bool IsRowVersion(IProperty property)
=> property.IsConcurrencyToken
&& property.ValueGenerated == ValueGenerated.OnAddOrUpdate;

private static FluentApiCodeFragment? GenerateAnnotations(
IAnnotatable annotatable,
Dictionary<string, IAnnotation> annotations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>("Id");
x.Property<byte[]>("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<int>("Id");
x.Property<int>("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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>("Id");
x.Property<byte[]>("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(
Expand Down
Loading