Skip to content

Commit

Permalink
Attribute for configuring composite primary keys (#27571)
Browse files Browse the repository at this point in the history
Co-authored-by: Shay Rojansky <roji@roji.org>
  • Loading branch information
ajcvickers and roji authored Mar 14, 2022
1 parent 9270925 commit 5ae9bb7
Show file tree
Hide file tree
Showing 16 changed files with 746 additions and 111 deletions.
45 changes: 45 additions & 0 deletions src/EFCore.Abstractions/PrimaryKeyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// Specifies a primary key for the entity type mapped to this CLR type.
/// </summary>
/// <remarks>
/// <para>
/// This attribute can be used for both keys made up of a
/// single property, and for composite keys made up of multiple properties. <see cref="KeyAttribute" />
/// can be used instead for single-property keys, in which case the behavior is identical. If both attributes are used, then
/// this attribute takes precedence.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see> for more information and
/// examples.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public sealed class PrimaryKeyAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PrimaryKeyAttribute" /> class.
/// </summary>
/// <param name="propertyName">The first (or only) property in the primary key.</param>
/// <param name="additionalPropertyNames">The additional properties which constitute the primary key, if any, in order.</param>
public PrimaryKeyAttribute(string propertyName, params string[] additionalPropertyNames)
{
Check.NotEmpty(propertyName, nameof(propertyName));
Check.HasNoEmptyElements(additionalPropertyNames, nameof(additionalPropertyNames));

PropertyNames = new List<string> { propertyName };
((List<string>)PropertyNames).AddRange(additionalPropertyNames);
}

/// <summary>
/// The properties which constitute the primary key, in order.
/// </summary>
public IReadOnlyList<string> PropertyNames { get; }
}
12 changes: 12 additions & 0 deletions src/EFCore/Metadata/Builders/IConventionEntityTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ bool CanHaveIndexerProperty(
/// </returns>
IConventionKeyBuilder? PrimaryKey(IReadOnlyList<IConventionProperty>? properties, bool fromDataAnnotation = false);

/// <summary>
/// Sets the properties that make up the primary key for this entity type.
/// </summary>
/// <param name="propertyNames">The names of the properties that make up the primary key.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>An object that can be used to configure the primary key.</returns>
/// <returns>
/// An object that can be used to configure the primary key if it was set on the entity type,
/// <see langword="null" /> otherwise.
/// </returns>
IConventionKeyBuilder? PrimaryKey(IReadOnlyList<string>? propertyNames, bool fromDataAnnotation = false);

/// <summary>
/// Returns a value indicating whether the given properties can be set as the primary key for this entity type.
/// </summary>
Expand Down
29 changes: 15 additions & 14 deletions src/EFCore/Metadata/Conventions/IndexAttributeConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public IndexAttributeConvention(ProviderConventionSetBuilderDependencies depende
public virtual void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> CheckIndexAttributesAndEnsureIndex(entityTypeBuilder.Metadata, false);
=> CheckIndexAttributesAndEnsureIndex(entityTypeBuilder.Metadata, shouldThrow: false);

/// <inheritdoc />
public virtual void ProcessEntityTypeBaseTypeChanged(
Expand All @@ -43,7 +43,7 @@ public virtual void ProcessEntityTypeBaseTypeChanged(
return;
}

CheckIndexAttributesAndEnsureIndex(entityTypeBuilder.Metadata, false);
CheckIndexAttributesAndEnsureIndex(entityTypeBuilder.Metadata, shouldThrow: false);
}

/// <inheritdoc />
Expand All @@ -53,7 +53,7 @@ public virtual void ProcessModelFinalizing(
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
{
CheckIndexAttributesAndEnsureIndex(entityType, true);
CheckIndexAttributesAndEnsureIndex(entityType, shouldThrow: true);
}
}

Expand All @@ -62,7 +62,7 @@ private static void CheckIndexAttributesAndEnsureIndex(
bool shouldThrow)
{
foreach (var indexAttribute in
entityType.ClrType.GetCustomAttributes<IndexAttribute>(true))
entityType.ClrType.GetCustomAttributes<IndexAttribute>(inherit: true))
{
IConventionIndexBuilder? indexBuilder;
if (!shouldThrow)
Expand Down Expand Up @@ -100,15 +100,18 @@ private static void CheckIndexAttributesAndEnsureIndex(
}
catch (InvalidOperationException exception)
{
CheckMissingProperties(indexAttribute, entityType, exception);
CheckMissingProperties(entityType, indexAttribute, exception);

throw;
}
}

if (indexBuilder == null)
{
CheckIgnoredProperties(indexAttribute, entityType);
if (shouldThrow)
{
CheckIgnoredProperties(entityType, indexAttribute);
}
}
else
{
Expand All @@ -119,15 +122,13 @@ private static void CheckIndexAttributesAndEnsureIndex(

if (indexBuilder is not null && indexAttribute.IsDescending is not null)
{
indexBuilder = indexBuilder.IsDescending(indexAttribute.IsDescending, fromDataAnnotation: true);
indexBuilder.IsDescending(indexAttribute.IsDescending, fromDataAnnotation: true);
}
}
}
}

private static void CheckIgnoredProperties(
IndexAttribute indexAttribute,
IConventionEntityType entityType)
private static void CheckIgnoredProperties(IConventionEntityType entityType, IndexAttribute indexAttribute)
{
foreach (var propertyName in indexAttribute.PropertyNames)
{
Expand All @@ -153,9 +154,9 @@ private static void CheckIgnoredProperties(
}

private static void CheckMissingProperties(
IndexAttribute indexAttribute,
IConventionEntityType entityType,
InvalidOperationException innerException)
IndexAttribute indexAttribute,
InvalidOperationException exception)
{
foreach (var propertyName in indexAttribute.PropertyNames)
{
Expand All @@ -169,7 +170,7 @@ private static void CheckMissingProperties(
entityType.DisplayName(),
indexAttribute.PropertyNames.Format(),
propertyName),
innerException);
exception);
}

throw new InvalidOperationException(
Expand All @@ -178,7 +179,7 @@ private static void CheckMissingProperties(
entityType.DisplayName(),
indexAttribute.PropertyNames.Format(),
propertyName),
innerException);
exception);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public virtual ConventionSet CreateConventionSet()
var foreignKeyAttributeConvention = new ForeignKeyAttributeConvention(Dependencies);
var relationshipDiscoveryConvention = new RelationshipDiscoveryConvention(Dependencies);
var servicePropertyDiscoveryConvention = new ServicePropertyDiscoveryConvention(Dependencies);
var keyAttributeConvention = new KeyAttributeConvention(Dependencies);
var indexAttributeConvention = new IndexAttributeConvention(Dependencies);
var baseTypeDiscoveryConvention = new BaseTypeDiscoveryConvention(Dependencies);
conventionSet.EntityTypeAddedConventions.Add(new NotMappedEntityTypeAttributeConvention(Dependencies));
Expand All @@ -70,6 +71,7 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.EntityTypeAddedConventions.Add(baseTypeDiscoveryConvention);
conventionSet.EntityTypeAddedConventions.Add(propertyDiscoveryConvention);
conventionSet.EntityTypeAddedConventions.Add(servicePropertyDiscoveryConvention);
conventionSet.EntityTypeAddedConventions.Add(keyAttributeConvention);
conventionSet.EntityTypeAddedConventions.Add(keyDiscoveryConvention);
conventionSet.EntityTypeAddedConventions.Add(indexAttributeConvention);
conventionSet.EntityTypeAddedConventions.Add(inversePropertyAttributeConvention);
Expand All @@ -87,6 +89,7 @@ public virtual ConventionSet CreateConventionSet()

conventionSet.EntityTypeBaseTypeChangedConventions.Add(propertyDiscoveryConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(servicePropertyDiscoveryConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(keyAttributeConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(keyDiscoveryConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(indexAttributeConvention);
conventionSet.EntityTypeBaseTypeChangedConventions.Add(inversePropertyAttributeConvention);
Expand All @@ -102,7 +105,6 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.EntityTypeMemberIgnoredConventions.Add(keyDiscoveryConvention);
conventionSet.EntityTypeMemberIgnoredConventions.Add(foreignKeyPropertyDiscoveryConvention);

var keyAttributeConvention = new KeyAttributeConvention(Dependencies);
var backingFieldConvention = new BackingFieldConvention(Dependencies);
var concurrencyCheckAttributeConvention = new ConcurrencyCheckAttributeConvention(Dependencies);
var databaseGeneratedAttributeConvention = new DatabaseGeneratedAttributeConvention(Dependencies);
Expand Down
Loading

0 comments on commit 5ae9bb7

Please sign in to comment.