Skip to content

Commit

Permalink
Metadata: Add Fluent API to configure Indexed properties
Browse files Browse the repository at this point in the history
Resolves #13610
  • Loading branch information
smitpatel committed Jan 4, 2020
1 parent 769a462 commit 6594428
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 19 deletions.
33 changes: 33 additions & 0 deletions src/EFCore/Metadata/Builders/EntityTypeBuilder.cs
Expand Up @@ -180,6 +180,39 @@ public virtual PropertyBuilder Property([NotNull] Type propertyType, [NotNull] s
Check.NotNull(propertyType, nameof(propertyType)),
Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit).Metadata);

/// <summary>
/// <para>
/// Returns an object that can be used to configure a property of the entity type.
/// If no property with the given name exists, then a new property will be added.
/// </para>
/// </summary>
/// <typeparam name="TProperty"> The type of the property to be configured. </typeparam>
/// <param name="propertyName"> The name of the property to be configured. </param>
/// <returns> An object that can be used to configure the property. </returns>
public virtual PropertyBuilder<TProperty> IndexedProperty<TProperty>([NotNull] string propertyName)
=> new PropertyBuilder<TProperty>(
Builder.IndexedProperty(
typeof(TProperty),
Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit).Metadata);

/// <summary>
/// <para>
/// Returns an object that can be used to configure a property of the entity type.
/// If no property with the given name exists, then a new property will be added.
/// </para>
/// <para>
/// Indexed properties are stored in the entity using an indexer supplying the provided property name.
/// </para>
/// </summary>
/// <param name="propertyType"> The type of the property to be configured. </param>
/// <param name="propertyName"> The name of the property to be configured. </param>
/// <returns> An object that can be used to configure the property. </returns>
public virtual PropertyBuilder IndexedProperty([NotNull] Type propertyType, [NotNull] string propertyName)
=> new PropertyBuilder(
Builder.IndexedProperty(
Check.NotNull(propertyType, nameof(propertyType)),
Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit).Metadata);

/// <summary>
/// Excludes the given property from the entity type. This method is typically used to remove properties
/// and navigations from the entity type that were added by convention.
Expand Down
35 changes: 28 additions & 7 deletions src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs
Expand Up @@ -10,6 +10,7 @@
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
Expand Down Expand Up @@ -438,6 +439,27 @@ public virtual InternalPropertyBuilder Property([NotNull] string propertyName, C
public virtual InternalPropertyBuilder Property([NotNull] MemberInfo memberInfo, ConfigurationSource? configurationSource)
=> Property(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName(), memberInfo, configurationSource, configurationSource);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual InternalPropertyBuilder IndexedProperty(
[CanBeNull] Type propertyType,
[NotNull] string propertyName,
ConfigurationSource? configurationSource)
{
var indexerPropertyInfo = Metadata.FindIndexerPropertyInfo();
if (indexerPropertyInfo == null)
{
throw new InvalidOperationException(
CoreStrings.NonIndexerEntityType(propertyName, Metadata.DisplayName(), typeof(string).ShortDisplayName()));
}

return Property(propertyType, propertyName, indexerPropertyInfo, configurationSource, configurationSource);
}

private InternalPropertyBuilder Property(
[CanBeNull] Type propertyType,
[NotNull] string propertyName,
Expand Down Expand Up @@ -596,9 +618,8 @@ public virtual InternalPropertyBuilder Property([NotNull] MemberInfo memberInfo,
}
}

property = clrProperty != null
? Metadata.AddProperty(clrProperty, configurationSource.Value)
: Metadata.AddProperty(propertyName, propertyType, typeConfigurationSource, configurationSource.Value);
property = Metadata.AddProperty(
propertyName, propertyType, clrProperty, typeConfigurationSource, configurationSource.Value);
}
}
else
Expand All @@ -618,9 +639,8 @@ public virtual InternalPropertyBuilder Property([NotNull] MemberInfo memberInfo,
var detachedProperties = DetachProperties(
new[] { existingProperty });

property = clrProperty != null
? Metadata.AddProperty(clrProperty, configurationSource.Value)
: Metadata.AddProperty(propertyName, propertyType, typeConfigurationSource, configurationSource.Value);
property = Metadata.AddProperty(
propertyName, propertyType, clrProperty, typeConfigurationSource, configurationSource.Value);

detachedProperties.Attach(this);
}
Expand Down Expand Up @@ -3471,7 +3491,8 @@ private void RemoveUnusedDiscriminatorProperty(Property newDiscriminatorProperty
/// </summary>
IConventionEntityType IConventionEntityTypeBuilder.Metadata
{
[DebuggerStepThrough] get => Metadata;
[DebuggerStepThrough]
get => Metadata;
}

/// <summary>
Expand Down
8 changes: 0 additions & 8 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions src/EFCore/Properties/CoreStrings.resx
Expand Up @@ -1081,9 +1081,6 @@
<data name="FkAttributeOnNonUniquePrincipal" xml:space="preserve">
<value>The ForeignKeyAttribute for the navigation '{navigation}' cannot be specified on the entity type '{principalType}' since it represents a one-to-many relationship. Move the ForeignKeyAttribute to a property on '{dependentType}'.</value>
</data>
<data name="NoIndexer" xml:space="preserve">
<value>An indexed property was added to entity type '{entity}'. But there is no public indexer on '{entity}' taking a single argument of type 'string' and returning type 'object'.</value>
</data>
<data name="ConstructorBindingFailed" xml:space="preserve">
<value>cannot bind '{failedBinds}' in '{parameters}'</value>
</data>
Expand Down
Expand Up @@ -247,7 +247,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
b =>
{
b.HasKey(c => c.Name);
b.Metadata.AddIndexedProperty("Nation", typeof(string));
b.IndexedProperty<string>("Nation");
});

modelBuilder.Entity<Gear>(
Expand Down
Expand Up @@ -1420,6 +1420,93 @@ public void Property_returns_same_instance_if_type_matches()
Assert.Equal(new[] { propertyBuilder.Metadata }, entityBuilder.GetActualProperties(new[] { propertyBuilder.Metadata }, null));
}

[ConditionalFact]
public void Can_add_indexed_property()
{
var modelBuilder = CreateModelBuilder();
var entityBuilder = modelBuilder.Entity(typeof(IndexedClass), ConfigurationSource.Explicit);

var propertyBuilder = entityBuilder.IndexedProperty(
typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.DataAnnotation);

Assert.NotNull(propertyBuilder);
}

[ConditionalFact]
public void Property_returns_same_instance_for_existing_index_property()
{
var modelBuilder = CreateModelBuilder();
var entityBuilder = modelBuilder.Entity(typeof(IndexedClass), ConfigurationSource.Explicit);

var propertyBuilder = entityBuilder.IndexedProperty(
typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.DataAnnotation);

Assert.NotNull(propertyBuilder);
Assert.Same(
propertyBuilder,
entityBuilder.Property(typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.Convention));

Assert.Same(
propertyBuilder,
entityBuilder.Property(IndexedClass.IndexedPropertyName, ConfigurationSource.Convention));

Assert.Null(entityBuilder.Property(typeof(int), IndexedClass.IndexedPropertyName, ConfigurationSource.Convention));
}

[ConditionalFact]
public void Property_removes_existing_index_property_for_higher_source_if_type_mismatch()
{
var modelBuilder = CreateModelBuilder();
var entityBuilder = modelBuilder.Entity(typeof(IndexedClass), ConfigurationSource.Explicit);

var propertyBuilder = entityBuilder.IndexedProperty(
typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.Convention);

Assert.NotNull(propertyBuilder);

var replacedPropertyBuilder = entityBuilder.Property(
typeof(int), IndexedClass.IndexedPropertyName, ConfigurationSource.DataAnnotation);

Assert.NotNull(replacedPropertyBuilder);
Assert.NotSame(propertyBuilder, replacedPropertyBuilder);
Assert.False(replacedPropertyBuilder.Metadata.IsIndexerProperty());
Assert.True(replacedPropertyBuilder.Metadata.IsShadowProperty());
}

[ConditionalFact]
public void Indexer_property_removes_existing_shadow_property_for_higher_source()
{
var modelBuilder = CreateModelBuilder();
var entityBuilder = modelBuilder.Entity(typeof(IndexedClass), ConfigurationSource.Explicit);

var shadowPropertyBuilder = entityBuilder.Property(
typeof(int), IndexedClass.IndexedPropertyName, ConfigurationSource.Convention);

Assert.NotNull(shadowPropertyBuilder);
Assert.True(shadowPropertyBuilder.Metadata.IsShadowProperty());

var replacedPropertyBuilder = entityBuilder.IndexedProperty(
typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.DataAnnotation);

Assert.NotNull(replacedPropertyBuilder);
Assert.NotSame(shadowPropertyBuilder, replacedPropertyBuilder);
Assert.True(replacedPropertyBuilder.Metadata.IsIndexerProperty());
Assert.False(replacedPropertyBuilder.Metadata.IsShadowProperty());
}

[ConditionalFact]
public void Indexed_property_throws_when_entityType_is_not_indexer()
{
var modelBuilder = CreateModelBuilder();
var entityBuilder = modelBuilder.Entity(typeof(Order), ConfigurationSource.Explicit);

Assert.Equal(
CoreStrings.NonIndexerEntityType(IndexedClass.IndexedPropertyName, nameof(Order), typeof(string).ShortDisplayName()),
Assert.Throws<InvalidOperationException>(
() => entityBuilder.IndexedProperty(
typeof(string), IndexedClass.IndexedPropertyName, ConfigurationSource.Convention)).Message);
}

[ConditionalFact]
public void Property_throws_for_navigation()
{
Expand Down Expand Up @@ -3059,5 +3146,12 @@ private class Splow : Splot
private class Splod : Splow
{
}

private class IndexedClass
{
public static readonly string IndexedPropertyName = "Indexer";

public object this[string name] => null;
}
}
}

0 comments on commit 6594428

Please sign in to comment.