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