diff --git a/src/EFCore/Metadata/Conventions/NonNullableConventionBase.cs b/src/EFCore/Metadata/Conventions/NonNullableConventionBase.cs index 81bb83e8544..2d6baeb19b6 100644 --- a/src/EFCore/Metadata/Conventions/NonNullableConventionBase.cs +++ b/src/EFCore/Metadata/Conventions/NonNullableConventionBase.cs @@ -69,9 +69,9 @@ protected virtual bool IsNonNullableReferenceType( return false; } - // For C# 8.0 nullable types, the C# currently synthesizes a NullableAttribute that expresses nullability into assemblies - // it produces. If the model is spread across more than one assembly, there will be multiple versions of this attribute, - // so look for it by name, caching to avoid reflection on every check. + // For C# 8.0 nullable types, the C# compiler currently synthesizes a NullableAttribute that expresses nullability into + // assemblies it produces. If the model is spread across more than one assembly, there will be multiple versions of this + // attribute, so look for it by name, caching to avoid reflection on every check. // Note that this may change - if https://github.com/dotnet/corefx/issues/36222 is done we can remove all of this. // First look for NullableAttribute on the member itself @@ -114,6 +114,18 @@ protected virtual bool IsNonNullableReferenceType( if (state.NullableContextFlagFieldInfo?.GetValue(contextAttr) is byte flag) { + // We currently don't calculate support nullability for generic properties, since calculating that is complex + // (depends on the nullability of generic type argument). + // However, we special case Dictionary as it's used for property bags, and specifically don't identify its indexer + // as non-nullable. + if (memberInfo is PropertyInfo property + && property.IsIndexerProperty() + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + return false; + } + return state.TypeCache[type] = flag == 1; } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index d9e38b1c421..841efa177d3 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2367,7 +2367,7 @@ public static string BackingFieldOnIndexer([CanBeNull] object field, [CanBeNull] field, entityType, property); /// - /// The entity type '{entityType}' cannot be added to the model because a shared entity type with the same clr type already exists. + /// The entity type '{entityType}' cannot be added to the model because a shared entity type with the same CLR type already exists. /// public static string ClashingSharedType([CanBeNull] object entityType) => string.Format( diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index b10f4a7fd3f..b07d092f536 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1281,7 +1281,7 @@ Cannot set backing field '{field}' for the indexer property '{entityType}.{property}'. Indexer properties are not allowed to use a backing field. - The entity type '{entityType}' cannot be added to the model because a shared entity type with the same clr type already exists. + The entity type '{entityType}' cannot be added to the model because a shared entity type with the same CLR type already exists. The skip navigation '{skipNavigation}' cannot be removed because it is set as the inverse of the skip navigation '{inverseSkipNavigation}' on '{referencingEntityType}'. All referencing skip navigations must be removed before this skip navigation can be removed. diff --git a/test/EFCore.Specification.Tests/Query/NorthwindKeylessEntitiesQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindKeylessEntitiesQueryTestBase.cs index 982af1e7fb4..e94a0a5b9da 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindKeylessEntitiesQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindKeylessEntitiesQueryTestBase.cs @@ -161,7 +161,7 @@ where ov.Customer.Orders.Any() [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task KeylesEntity_groupby(bool async) + public virtual Task KeylessEntity_groupby(bool async) { return AssertQuery( async, diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindKeylessEntitiesQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindKeylessEntitiesQuerySqlServerTest.cs index 89c2591b4dd..82a9cdf355e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindKeylessEntitiesQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindKeylessEntitiesQuerySqlServerTest.cs @@ -147,9 +147,9 @@ public override void Auto_initialized_view_set() @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c]"); } - public override async Task KeylesEntity_groupby(bool async) + public override async Task KeylessEntity_groupby(bool async) { - await base.KeylesEntity_groupby(async); + await base.KeylessEntity_groupby(async); AssertSql( @"SELECT [c].[City] AS [Key], COUNT(*) AS [Count], COALESCE(SUM(CAST(LEN([c].[Address]) AS int)), 0) AS [Sum] diff --git a/test/EFCore.Tests/Metadata/Conventions/NonNullableReferencePropertyConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/NonNullableReferencePropertyConventionTest.cs index 866aa97772a..072e74cd68f 100644 --- a/test/EFCore.Tests/Metadata/Conventions/NonNullableReferencePropertyConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/NonNullableReferencePropertyConventionTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -82,6 +83,15 @@ public void Reference_nullability_sets_is_nullable_correctly(Type type, string p Assert.Equal(expectedNullable, entityTypeBuilder.Property(propertyName).Metadata.IsNullable); } + [ConditionalFact] + public void Non_nullability_ignores_context_on_generic_types() + { + var modelBuilder = CreateModelBuilder(); + var entityTypeBuilder = modelBuilder.SharedTypeEntity>("SomeBag"); + entityTypeBuilder.IndexerProperty(typeof(string), "b"); + Assert.True(entityTypeBuilder.Property("b").Metadata.IsNullable); + } + private void RunConvention(InternalPropertyBuilder propertyBuilder) { var context = new ConventionContext(