diff --git a/src/EFCore.Abstractions/BackingFieldAttribute.cs b/src/EFCore.Abstractions/BackingFieldAttribute.cs new file mode 100644 index 00000000000..7d203971bc4 --- /dev/null +++ b/src/EFCore.Abstractions/BackingFieldAttribute.cs @@ -0,0 +1,32 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Names the backing field associated with this property. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class BackingFieldAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the backing field. + public BackingFieldAttribute([NotNull] string name) + { + Check.NotEmpty(name, nameof(name)); + + Name = name; + } + + /// + /// The name of the backing field. + /// + public string Name { get; } + } +} diff --git a/src/EFCore/Metadata/Conventions/BackingFieldAttributeConvention.cs b/src/EFCore/Metadata/Conventions/BackingFieldAttributeConvention.cs new file mode 100644 index 00000000000..c4ad656f438 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/BackingFieldAttributeConvention.cs @@ -0,0 +1,42 @@ +// 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.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that configures a property as having a backing field + /// based on the attribute. + /// + public class BackingFieldAttributeConvention : PropertyAttributeConventionBase + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public BackingFieldAttributeConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) + { + } + + /// + /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. + /// + /// The builder for the property. + /// The attribute. + /// The member that has the attribute. + /// Additional information associated with convention execution. + protected override void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + BackingFieldAttribute attribute, + MemberInfo clrMember, + IConventionContext context) + { + propertyBuilder.HasField(attribute.Name, fromDataAnnotation: true); + } + } +} diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 9ffcffdaf3f..ea3ffde0637 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -109,6 +109,7 @@ public virtual ConventionSet CreateConventionSet() var maxLengthAttributeConvention = new MaxLengthAttributeConvention(Dependencies); var stringLengthAttributeConvention = new StringLengthAttributeConvention(Dependencies); var timestampAttributeConvention = new TimestampAttributeConvention(Dependencies); + var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies); conventionSet.PropertyAddedConventions.Add(backingFieldConvention); conventionSet.PropertyAddedConventions.Add(concurrencyCheckAttributeConvention); @@ -118,6 +119,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(maxLengthAttributeConvention); conventionSet.PropertyAddedConventions.Add(stringLengthAttributeConvention); conventionSet.PropertyAddedConventions.Add(timestampAttributeConvention); + conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(keyAttributeConvention); conventionSet.PropertyAddedConventions.Add(keyDiscoveryConvention); conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); diff --git a/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs index 7a7b5c65a8a..537aec09ff6 100644 --- a/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/PropertyAttributeConventionTest.cs @@ -509,6 +509,34 @@ public void TimestampAttribute_on_field_sets_concurrency_token_with_conventional Assert.True(entityTypeBuilder.Property(nameof(F.Timestamp)).Metadata.IsConcurrencyToken); } + [ConditionalFact] + public void BackingFieldAttribute_overrides_configuration_from_convention_source() + { + var entityTypeBuilder = CreateInternalEntityTypeBuilder(); + + var propertyBuilder = entityTypeBuilder.Property(typeof(int?), "BackingFieldProperty", ConfigurationSource.Explicit); + + RunConvention(propertyBuilder); + + // also asserts that the default backing field, _backingFieldProperty, was _not_ chosen + Assert.Equal("_backingFieldForAttribute", propertyBuilder.Metadata.GetFieldName()); + } + + [ConditionalFact] + public void BackingFieldAttribute_does_not_override_configuration_from_explicit_source() + { + var entityTypeBuilder = CreateInternalEntityTypeBuilder(); + + var propertyBuilder = entityTypeBuilder.Property(typeof(int?), "BackingFieldProperty", ConfigurationSource.Explicit); + + propertyBuilder.HasField("_backingFieldForFluentApi", ConfigurationSource.Explicit); + + RunConvention(propertyBuilder); + + // also asserts that the default backing field, _backingFieldProperty, was _not_ chosen + Assert.Equal("_backingFieldForFluentApi", propertyBuilder.Metadata.GetFieldName()); + } + #endregion [ConditionalFact] @@ -537,6 +565,9 @@ private static void RunConvention(InternalPropertyBuilder propertyBuilder) var context = new ConventionContext( propertyBuilder.Metadata.DeclaringEntityType.Model.ConventionDispatcher); + new BackingFieldConvention(dependencies) + .ProcessPropertyAdded(propertyBuilder, context); + new ConcurrencyCheckAttributeConvention(dependencies) .ProcessPropertyAdded(propertyBuilder, context); @@ -557,6 +588,9 @@ private static void RunConvention(InternalPropertyBuilder propertyBuilder) new TimestampAttributeConvention(dependencies) .ProcessPropertyAdded(propertyBuilder, context); + + new BackingFieldAttributeConvention(dependencies) + .ProcessPropertyAdded(propertyBuilder, context); } private void RunConvention(InternalEntityTypeBuilder entityTypeBuilder) @@ -607,6 +641,22 @@ private class A [Required] private int? PrivateProperty { get; set; } + + private int? _backingFieldProperty; // selected by convention + private int? _backingFieldForAttribute; + private int? _backingFieldForFluentApi; + + [BackingField("_backingFieldForAttribute")] + private int? BackingFieldProperty + { + get => _backingFieldForAttribute; + set + { + _backingFieldProperty = value; + _backingFieldForAttribute = value; + _backingFieldForFluentApi = value; + } + } } private class B