diff --git a/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs b/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs
deleted file mode 100644
index 0d64f18fe35..00000000000
--- a/src/EFCore.Cosmos/Query/CosmosQueryTranslationPostprocessor.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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.Linq.Expressions;
-using JetBrains.Annotations;
-using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
-using Microsoft.EntityFrameworkCore.Query;
-using Microsoft.EntityFrameworkCore.Utilities;
-
-namespace Microsoft.EntityFrameworkCore.Cosmos.Query
-{
- ///
- public class CosmosQueryTranslationPostprocessor : QueryTranslationPostprocessor
- {
- private readonly ISqlExpressionFactory _sqlExpressionFactory;
-
- ///
- /// Creates a new instance of the class.
- ///
- /// Parameter object containing dependencies for this class.
- /// The SqlExpressionFactory object to use.
- /// The query compilation context object to use.
- public CosmosQueryTranslationPostprocessor(
- [NotNull] QueryTranslationPostprocessorDependencies dependencies,
- [NotNull] ISqlExpressionFactory sqlExpressionFactory,
- [NotNull] QueryCompilationContext queryCompilationContext)
- : base(dependencies, queryCompilationContext)
- {
- Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory));
-
- _sqlExpressionFactory = sqlExpressionFactory;
- }
-
- ///
- public override Expression Process(Expression query)
- {
- query = base.Process(query);
- query = new CosmosValueConverterCompensatingExpressionVisitor(_sqlExpressionFactory).Visit(query);
-
- return query;
- }
- }
-}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs
new file mode 100644
index 00000000000..07341c85951
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPostprocessor.cs
@@ -0,0 +1,52 @@
+// 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.Linq.Expressions;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore.Query;
+using Microsoft.EntityFrameworkCore.Utilities;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class CosmosQueryTranslationPostprocessor : QueryTranslationPostprocessor
+ {
+ private readonly ISqlExpressionFactory _sqlExpressionFactory;
+
+ ///
+ /// 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.
+ ///
+ public CosmosQueryTranslationPostprocessor(
+ [NotNull] QueryTranslationPostprocessorDependencies dependencies,
+ [NotNull] ISqlExpressionFactory sqlExpressionFactory,
+ [NotNull] QueryCompilationContext queryCompilationContext)
+ : base(dependencies, queryCompilationContext)
+ {
+ Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory));
+
+ _sqlExpressionFactory = sqlExpressionFactory;
+ }
+
+ ///
+ /// 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.
+ ///
+ public override Expression Process(Expression query)
+ {
+ query = base.Process(query);
+ query = new CosmosValueConverterCompensatingExpressionVisitor(_sqlExpressionFactory).Visit(query);
+
+ return query;
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs
index 6d2ca88555e..147b0ee190e 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs
@@ -23,7 +23,7 @@ public CosmosProjectionBindingRemovingExpressionVisitor(
{
_selectExpression = selectExpression;
}
-
+
protected override ProjectionExpression GetProjection(ProjectionBindingExpression projectionBindingExpression)
=> _selectExpression.Projection[GetProjectionIndex(projectionBindingExpression)];
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs
index a583b0b30d4..266f5aea8e3 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs
@@ -483,8 +483,16 @@ MethodInfo GetMethod()
return new EntityReferenceExpression(subqueryTranslation);
}
+ var shaperExpression = subqueryTranslation.ShaperExpression;
+ if (shaperExpression is UnaryExpression unaryExpression
+ && unaryExpression.NodeType == ExpressionType.Convert
+ && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type)
+ {
+ shaperExpression = unaryExpression.Operand;
+ }
+
#pragma warning disable IDE0046 // Convert to conditional expression
- if (!(subqueryTranslation.ShaperExpression is ProjectionBindingExpression projectionBindingExpression))
+ if (!(shaperExpression is ProjectionBindingExpression projectionBindingExpression))
#pragma warning restore IDE0046 // Convert to conditional expression
{
return null;
@@ -869,7 +877,6 @@ private Expression TryBindMember(Expression source, MemberIdentity member, Type
if (property != null)
{
return BindProperty(entityReferenceExpression, property, type);
-
}
AddTranslationErrorDetails(
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs
index 41844ab73a6..dcbc3cec828 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
+using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -79,6 +80,8 @@ public virtual Expression Translate([NotNull] InMemoryQueryExpression queryExpre
_projectionMapping.Clear();
_projectionMembers.Clear();
+ result = MatchTypes(result, expression.Type);
+
return result;
}
@@ -164,18 +167,10 @@ public override Expression Visit(Expression expression)
}
var translation = _expressionTranslatingExpressionVisitor.Translate(expression);
- if (translation == null)
- {
- return base.Visit(expression);
- }
-
- if (translation.Type != expression.Type)
- {
- translation = NullSafeConvert(translation, expression.Type);
- }
-
- return new ProjectionBindingExpression(
- _queryExpression, _queryExpression.AddToProjection(translation), expression.Type);
+ return translation == null
+ ? base.Visit(expression)
+ : new ProjectionBindingExpression(
+ _queryExpression, _queryExpression.AddToProjection(translation), expression.Type.MakeNullable());
}
else
{
@@ -185,37 +180,48 @@ public override Expression Visit(Expression expression)
return null;
}
- if (translation.Type != expression.Type)
- {
- translation = NullSafeConvert(translation, expression.Type);
- }
-
_projectionMapping[_projectionMembers.Peek()] = translation;
- return new ProjectionBindingExpression(_queryExpression, _projectionMembers.Peek(), expression.Type);
+ return new ProjectionBindingExpression(_queryExpression, _projectionMembers.Peek(), expression.Type.MakeNullable());
}
}
return base.Visit(expression);
}
- private Expression NullSafeConvert(Expression expression, Type convertTo)
- => expression.Type.IsNullableType() && !convertTo.IsNullableType() && expression.Type.UnwrapNullableType() == convertTo
- ? (Expression)Expression.Coalesce(expression, Expression.Default(convertTo))
- : Expression.Convert(expression, convertTo);
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ var left = MatchTypes(Visit(binaryExpression.Left), binaryExpression.Left.Type);
+ var right = MatchTypes(Visit(binaryExpression.Right), binaryExpression.Right.Type);
- private CollectionShaperExpression AddCollectionProjection(
- ShapedQueryExpression subquery, INavigationBase navigation, Type elementType)
- => new CollectionShaperExpression(
- new ProjectionBindingExpression(
- _queryExpression,
- _queryExpression.AddSubqueryProjection(
- subquery,
- out var innerShaper),
- typeof(IEnumerable)),
- innerShaper,
- navigation,
- elementType);
+ return binaryExpression.Update(left, VisitAndConvert(binaryExpression.Conversion, "VisitBinary"), right);
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitConditional(ConditionalExpression conditionalExpression)
+ {
+ var test = Visit(conditionalExpression.Test);
+ var ifTrue = Visit(conditionalExpression.IfTrue);
+ var ifFalse = Visit(conditionalExpression.IfFalse);
+
+ if (test.Type == typeof(bool?))
+ {
+ test = Expression.Equal(test, Expression.Constant(true, typeof(bool?)));
+ }
+
+ return conditionalExpression.Update(test, ifTrue, ifFalse);
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -269,43 +275,69 @@ protected override Expression VisitExtension(Expression extensionExpression)
/// 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.
///
- protected override Expression VisitNew(NewExpression newExpression)
+ protected override ElementInit VisitElementInit(ElementInit elementInit)
+ => elementInit.Update(elementInit.Arguments.Select(e => MatchTypes(Visit(e), e.Type)));
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitMember(MemberExpression memberExpression)
{
- Check.NotNull(newExpression, nameof(newExpression));
+ var expression = Visit(memberExpression.Expression);
+ Expression updatedMemberExpression = memberExpression.Update(
+ expression != null ? MatchTypes(expression, memberExpression.Expression.Type) : expression);
- if (newExpression.Arguments.Count == 0)
+ if (expression?.Type.IsNullableValueType() == true)
{
- return newExpression;
+ var nullableReturnType = memberExpression.Type.MakeNullable();
+ if (!memberExpression.Type.IsNullableType())
+ {
+ updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType);
+ }
+
+ updatedMemberExpression = Expression.Condition(
+ Expression.Equal(expression, Expression.Default(expression.Type)),
+ Expression.Constant(null, nullableReturnType),
+ updatedMemberExpression);
}
- if (!_clientEval
- && newExpression.Members == null)
+ return updatedMemberExpression;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment)
+ {
+ var expression = memberAssignment.Expression;
+ Expression visitedExpression;
+ if (_clientEval)
{
- return null;
+ visitedExpression = Visit(memberAssignment.Expression);
}
-
- var newArguments = new Expression[newExpression.Arguments.Count];
- for (var i = 0; i < newArguments.Length; i++)
+ else
{
- if (_clientEval)
+ var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member);
+ _projectionMembers.Push(projectionMember);
+
+ visitedExpression = Visit(memberAssignment.Expression);
+ if (visitedExpression == null)
{
- newArguments[i] = Visit(newExpression.Arguments[i]);
+ return null;
}
- else
- {
- var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]);
- _projectionMembers.Push(projectionMember);
- newArguments[i] = Visit(newExpression.Arguments[i]);
- if (newArguments[i] == null)
- {
- return null;
- }
- _projectionMembers.Pop();
- }
+ _projectionMembers.Pop();
}
- return newExpression.Update(newArguments);
+ visitedExpression = MatchTypes(visitedExpression, expression.Type);
+
+ return memberAssignment.Update(visitedExpression);
}
///
@@ -348,24 +380,111 @@ protected override Expression VisitMemberInit(MemberInitExpression memberInitExp
/// 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.
///
- protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment)
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
- if (_clientEval)
+ var @object = Visit(methodCallExpression.Object);
+ var arguments = new Expression[methodCallExpression.Arguments.Count];
+ for (var i = 0; i < methodCallExpression.Arguments.Count; i++)
{
- return memberAssignment.Update(Visit(memberAssignment.Expression));
+ var argument = methodCallExpression.Arguments[i];
+ arguments[i] = MatchTypes(Visit(argument), argument.Type);
+ }
+
+ Expression updatedMethodCallExpression = methodCallExpression.Update(
+ @object != null ? MatchTypes(@object, methodCallExpression.Object.Type) : @object,
+ arguments);
+
+ if (@object?.Type.IsNullableType() == true
+ && !methodCallExpression.Object.Type.IsNullableType())
+ {
+ var nullableReturnType = methodCallExpression.Type.MakeNullable();
+ if (!methodCallExpression.Type.IsNullableType())
+ {
+ updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType);
+ }
+
+ return Expression.Condition(
+ Expression.Equal(@object, Expression.Default(@object.Type)),
+ Expression.Constant(null, nullableReturnType),
+ updatedMethodCallExpression);
}
- var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member);
- _projectionMembers.Push(projectionMember);
+ return updatedMethodCallExpression;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitNew(NewExpression newExpression)
+ {
+ Check.NotNull(newExpression, nameof(newExpression));
+
+ if (newExpression.Arguments.Count == 0)
+ {
+ return newExpression;
+ }
- var visitedExpression = Visit(memberAssignment.Expression);
- if (visitedExpression == null)
+ if (!_clientEval
+ && newExpression.Members == null)
{
return null;
}
- _projectionMembers.Pop();
- return memberAssignment.Update(visitedExpression);
+ var newArguments = new Expression[newExpression.Arguments.Count];
+ for (var i = 0; i < newArguments.Length; i++)
+ {
+ var argument = newExpression.Arguments[i];
+ Expression visitedArgument;
+ if (_clientEval)
+ {
+ visitedArgument = Visit(argument);
+ }
+ else
+ {
+ var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]);
+ _projectionMembers.Push(projectionMember);
+ visitedArgument = Visit(argument);
+ if (visitedArgument == null)
+ {
+ return null;
+ }
+
+ _projectionMembers.Pop();
+ }
+
+ newArguments[i] = MatchTypes(visitedArgument, argument.Type);
+ }
+
+ return newExpression.Update(newArguments);
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
+ => newArrayExpression.Update(newArrayExpression.Expressions.Select(e => MatchTypes(Visit(e), e.Type)));
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitUnary(UnaryExpression unaryExpression)
+ {
+ var operand = Visit(unaryExpression.Operand);
+
+ return (unaryExpression.NodeType == ExpressionType.Convert
+ || unaryExpression.NodeType == ExpressionType.ConvertChecked)
+ && unaryExpression.Type == operand.Type
+ ? operand
+ : unaryExpression.Update(MatchTypes(operand, unaryExpression.Operand.Type));
}
// TODO: Debugging
@@ -376,5 +495,31 @@ private void VerifyQueryExpression(ProjectionBindingExpression projectionBinding
throw new InvalidOperationException(CoreStrings.QueryFailed(projectionBindingExpression.Print(), GetType().Name));
}
}
+
+ private CollectionShaperExpression AddCollectionProjection(
+ ShapedQueryExpression subquery, INavigationBase navigation, Type elementType)
+ => new CollectionShaperExpression(
+ new ProjectionBindingExpression(
+ _queryExpression,
+ _queryExpression.AddSubqueryProjection(
+ subquery,
+ out var innerShaper),
+ typeof(IEnumerable)),
+ innerShaper,
+ navigation,
+ elementType);
+
+ private static Expression MatchTypes(Expression expression, Type targetType)
+ {
+ if (targetType != expression.Type
+ && targetType.TryGetElementType(typeof(IQueryable<>)) == null)
+ {
+ Check.DebugAssert(targetType.MakeNullable() == expression.Type, "Not a nullable to non-nullable conversion");
+
+ expression = Expression.Convert(expression, targetType);
+ }
+
+ return expression;
+ }
}
}
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs
index ea56be5271f..9d14460626f 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs
@@ -88,25 +88,30 @@ public InMemoryQueryExpression([NotNull] IEntityType entityType)
_valueBufferParameter = Parameter(typeof(ValueBuffer), "valueBuffer");
ServerQueryExpression = new InMemoryTableExpression(entityType);
var readExpressionMap = new Dictionary();
+ var discriminatorProperty = entityType.GetDiscriminatorProperty();
foreach (var property in entityType.GetAllBaseTypesInclusive().SelectMany(et => et.GetDeclaredProperties()))
{
readExpressionMap[property] = CreateReadValueExpression(property.ClrType, property.GetIndex(), property);
}
- foreach (var property in entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredProperties()))
+ foreach (var derivedEntityType in entityType.GetDerivedTypes())
{
- readExpressionMap[property] = Condition(
- LessThan(
- Constant(property.GetIndex()),
- MakeMemberAccess(
- _valueBufferParameter,
- _valueBufferCountMemberInfo)),
- CreateReadValueExpression(property.ClrType, property.GetIndex(), property),
- Default(property.ClrType));
+ var entityCheck = derivedEntityType.GetConcreteDerivedTypesInclusive()
+ .Select(e => Equal(readExpressionMap[discriminatorProperty], Constant(e.GetDiscriminatorValue())))
+ .Aggregate((l, r) => OrElse(l, r));
+
+ foreach (var property in derivedEntityType.GetDeclaredProperties())
+ {
+ readExpressionMap[property] = Condition(
+ entityCheck,
+ CreateReadValueExpression(property.ClrType, property.GetIndex(), property),
+ Default(property.ClrType));
+ }
}
var entityProjection = new EntityProjectionExpression(entityType, readExpressionMap);
_projectionMapping[new ProjectionMember()] = entityProjection;
+
}
///
@@ -123,7 +128,7 @@ public virtual Expression GetSingleScalarProjection()
ConvertToEnumerable();
- return new ProjectionBindingExpression(this, new ProjectionMember(), expression.Type);
+ return new ProjectionBindingExpression(this, new ProjectionMember(), expression.Type.MakeNullable());
}
///
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs
index 3543db62faf..6bba55266e9 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs
@@ -141,7 +141,7 @@ protected override ShapedQueryExpression TranslateAll(ShapedQueryExpression sour
EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type),
inMemoryQueryExpression.ServerQueryExpression)));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool)));
}
///
@@ -170,11 +170,11 @@ protected override ShapedQueryExpression TranslateAny(ShapedQueryExpression sour
}
inMemoryQueryExpression.UpdateServerQueryExpression(
- Expression.Call(
- EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type),
- inMemoryQueryExpression.ServerQueryExpression));
+ Expression.Call(
+ EnumerableMethods.AnyWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type),
+ inMemoryQueryExpression.ServerQueryExpression));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool)));
}
///
@@ -188,7 +188,7 @@ protected override ShapedQueryExpression TranslateAverage(ShapedQueryExpression
Check.NotNull(source, nameof(source));
Check.NotNull(resultType, nameof(resultType));
- return TranslateScalarAggregate(source, selector, nameof(Enumerable.Average));
+ return TranslateScalarAggregate(source, selector, nameof(Enumerable.Average), resultType);
}
///
@@ -249,7 +249,7 @@ protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression
inMemoryQueryExpression.GetMappedProjection(new ProjectionMember()), inMemoryQueryExpression.CurrentParameter)),
item));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(bool)));
}
///
@@ -284,7 +284,7 @@ protected override ShapedQueryExpression TranslateCount(ShapedQueryExpression so
EnumerableMethods.CountWithoutPredicate.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type),
inMemoryQueryExpression.ServerQueryExpression));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(int)));
}
///
@@ -751,7 +751,7 @@ protected override ShapedQueryExpression TranslateLongCount(ShapedQueryExpressio
inMemoryQueryExpression.CurrentParameter.Type),
inMemoryQueryExpression.ServerQueryExpression));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), typeof(long)));
}
///
@@ -765,7 +765,7 @@ protected override ShapedQueryExpression TranslateMax(
{
Check.NotNull(source, nameof(source));
- return TranslateScalarAggregate(source, selector, nameof(Enumerable.Max));
+ return TranslateScalarAggregate(source, selector, nameof(Enumerable.Max), resultType);
}
///
@@ -778,7 +778,7 @@ protected override ShapedQueryExpression TranslateMin(ShapedQueryExpression sour
{
Check.NotNull(source, nameof(source));
- return TranslateScalarAggregate(source, selector, nameof(Enumerable.Min));
+ return TranslateScalarAggregate(source, selector, nameof(Enumerable.Min), resultType);
}
///
@@ -1095,7 +1095,7 @@ protected override ShapedQueryExpression TranslateSum(ShapedQueryExpression sour
Check.NotNull(source, nameof(source));
Check.NotNull(resultType, nameof(resultType));
- return TranslateScalarAggregate(source, selector, nameof(Enumerable.Sum));
+ return TranslateScalarAggregate(source, selector, nameof(Enumerable.Sum), resultType);
}
///
@@ -1435,7 +1435,7 @@ ProjectionBindingExpression projectionBindingExpression
}
private ShapedQueryExpression TranslateScalarAggregate(
- ShapedQueryExpression source, LambdaExpression selector, string methodName)
+ ShapedQueryExpression source, LambdaExpression selector, string methodName, Type returnType)
{
var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression;
@@ -1459,7 +1459,7 @@ private ShapedQueryExpression TranslateScalarAggregate(
inMemoryQueryExpression.UpdateServerQueryExpression(
Expression.Call(method, inMemoryQueryExpression.ServerQueryExpression, selector));
- return source.UpdateShaperExpression(inMemoryQueryExpression.GetSingleScalarProjection());
+ return source.UpdateShaperExpression(Expression.Convert(inMemoryQueryExpression.GetSingleScalarProjection(), returnType));
MethodInfo GetMethod()
=> methodName switch
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs
index 677f64cbe07..fce4c902944 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.InMemoryProjectionBindingRemovingExpressionVisitor.cs
@@ -1,6 +1,7 @@
// 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 System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
@@ -69,6 +70,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
_materializationContextBindings[
(ParameterExpression)((MethodCallExpression)methodCallExpression.Arguments[0]).Object];
+ Check.DebugAssert(property != null || methodCallExpression.Type.IsNullableType(), "Must read nullable value without property");
+
return Expression.Call(
methodCallExpression.Method,
valueBuffer,
@@ -88,11 +91,12 @@ protected override Expression VisitExtension(Expression extensionExpression)
var queryExpression = (InMemoryQueryExpression)projectionBindingExpression.QueryExpression;
var projectionIndex = (int)GetProjectionIndex(queryExpression, projectionBindingExpression);
var valueBuffer = queryExpression.CurrentParameter;
+ var property = InferPropertyFromInner(queryExpression.Projection[projectionIndex]);
+
+ Check.DebugAssert(property != null || projectionBindingExpression.Type.IsNullableType()
+ || projectionBindingExpression.Type == typeof(ValueBuffer), "Must read nullable value without property");
- return valueBuffer.CreateValueBufferReadValueExpression(
- projectionBindingExpression.Type,
- projectionIndex,
- InferPropertyFromInner(queryExpression.Projection[projectionIndex]));
+ return valueBuffer.CreateValueBufferReadValueExpression(projectionBindingExpression.Type, projectionIndex, property);
}
return base.VisitExtension(extensionExpression);
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 162407c7534..9d669ea6e33 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -943,6 +943,12 @@ public static string DefaultValueSqlUnspecified([CanBeNull] object column, [CanB
GetString("DefaultValueSqlUnspecified", nameof(column), nameof(table)),
column, table);
+ ///
+ /// Sequence contains no elements.
+ ///
+ public static string SequenceContainsNoElements
+ => GetString("SequenceContainsNoElements");
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 7c822104ecd..4b7e25267ce 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -688,4 +688,7 @@
The column '{column}' on table {table} has unspecified default value SQL. Specify the SQL before using EF Core to create the database schema.
+
+ Sequence contains no elements.
+
\ No newline at end of file
diff --git a/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs b/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs
index 13d858fcfdb..58f25daf294 100644
--- a/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs
+++ b/src/EFCore.Relational/Query/Internal/GetValueOrDefaultTranslator.cs
@@ -49,24 +49,12 @@ public virtual SqlExpression Translate(SqlExpression instance, MethodInfo method
return _sqlExpressionFactory.Coalesce(
instance,
arguments.Count == 0
- ? GetDefaultConstant(method.ReturnType)
+ ? new SqlConstantExpression(method.ReturnType.GetDefaultValueConstant(), null)
: arguments[0],
instance.TypeMapping);
}
return null;
}
-
- private SqlConstantExpression GetDefaultConstant(Type type)
- {
- return (SqlConstantExpression)_generateDefaultValueConstantMethod
- .MakeGenericMethod(type).Invoke(null, Array.Empty
public class RelationalProjectionBindingExpressionVisitor : ExpressionVisitor
{
+ private static readonly MethodInfo _getParameterValueMethodInfo
+ = typeof(RelationalProjectionBindingExpressionVisitor)
+ .GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue));
+
private readonly RelationalQueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor;
private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
@@ -83,6 +87,8 @@ public virtual Expression Translate([NotNull] SelectExpression selectExpression,
_projectionMembers.Clear();
_projectionMapping.Clear();
+ result = MatchTypes(result, expression.Type);
+
return result;
}
@@ -200,6 +206,10 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method)
}
if (!(subquery.ShaperExpression is ProjectionBindingExpression
+ || (subquery.ShaperExpression is UnaryExpression unaryExpression
+ && unaryExpression.NodeType == ExpressionType.Convert
+ && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type
+ && unaryExpression.Operand is ProjectionBindingExpression)
|| IsAggregateResultWithCustomShaper(methodCallExpression.Method)))
{
return _selectExpression.AddSingleProjection(subquery);
@@ -215,7 +225,7 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method)
return translation == null
? base.Visit(expression)
: new ProjectionBindingExpression(
- _selectExpression, _selectExpression.AddToProjection(translation), expression.Type);
+ _selectExpression, _selectExpression.AddToProjection(translation), expression.Type.MakeNullable());
}
else
{
@@ -227,21 +237,46 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method)
_projectionMapping[_projectionMembers.Peek()] = translation;
- return new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), expression.Type);
+ return new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), expression.Type.MakeNullable());
}
}
return base.Visit(expression);
}
- private static readonly MethodInfo _getParameterValueMethodInfo
- = typeof(RelationalProjectionBindingExpressionVisitor)
- .GetTypeInfo().GetDeclaredMethod(nameof(GetParameterValue));
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ var left = MatchTypes(Visit(binaryExpression.Left), binaryExpression.Left.Type);
+ var right = MatchTypes(Visit(binaryExpression.Right), binaryExpression.Right.Type);
-#pragma warning disable IDE0052 // Remove unread private members
- private static T GetParameterValue(QueryContext queryContext, string parameterName)
-#pragma warning restore IDE0052 // Remove unread private members
- => (T)queryContext.ParameterValues[parameterName];
+ return binaryExpression.Update(left, VisitAndConvert(binaryExpression.Conversion, "VisitBinary"), right);
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitConditional(ConditionalExpression conditionalExpression)
+ {
+ var test = Visit(conditionalExpression.Test);
+ var ifTrue = Visit(conditionalExpression.IfTrue);
+ var ifFalse = Visit(conditionalExpression.IfFalse);
+
+ if (test.Type == typeof(bool?))
+ {
+ test = Expression.Equal(test, Expression.Constant(true, typeof(bool?)));
+ }
+
+ return conditionalExpression.Update(test, ifTrue, ifFalse);
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -255,7 +290,6 @@ protected override Expression VisitExtension(Expression extensionExpression)
switch (extensionExpression)
{
-
case EntityShaperExpression entityShaperExpression:
{
// TODO: Make this easier to understand some day.
@@ -318,43 +352,69 @@ protected override Expression VisitExtension(Expression extensionExpression)
/// 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.
///
- protected override Expression VisitNew(NewExpression newExpression)
+ protected override ElementInit VisitElementInit(ElementInit elementInit)
+ => elementInit.Update(elementInit.Arguments.Select(e => MatchTypes(Visit(e), e.Type)));
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitMember(MemberExpression memberExpression)
{
- Check.NotNull(newExpression, nameof(newExpression));
+ var expression = Visit(memberExpression.Expression);
+ Expression updatedMemberExpression = memberExpression.Update(
+ expression != null ? MatchTypes(expression, memberExpression.Expression.Type) : expression);
- if (newExpression.Arguments.Count == 0)
+ if (expression?.Type.IsNullableValueType() == true)
{
- return newExpression;
+ var nullableReturnType = memberExpression.Type.MakeNullable();
+ if (!memberExpression.Type.IsNullableType())
+ {
+ updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType);
+ }
+
+ updatedMemberExpression = Expression.Condition(
+ Expression.Equal(expression, Expression.Default(expression.Type)),
+ Expression.Constant(null, nullableReturnType),
+ updatedMemberExpression);
}
- if (!_clientEval
- && newExpression.Members == null)
+ return updatedMemberExpression;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment)
+ {
+ var expression = memberAssignment.Expression;
+ Expression visitedExpression;
+ if (_clientEval)
{
- return null;
+ visitedExpression = Visit(memberAssignment.Expression);
}
-
- var newArguments = new Expression[newExpression.Arguments.Count];
- for (var i = 0; i < newArguments.Length; i++)
+ else
{
- if (_clientEval)
+ var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member);
+ _projectionMembers.Push(projectionMember);
+
+ visitedExpression = Visit(memberAssignment.Expression);
+ if (visitedExpression == null)
{
- newArguments[i] = Visit(newExpression.Arguments[i]);
+ return null;
}
- else
- {
- var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]);
- _projectionMembers.Push(projectionMember);
- newArguments[i] = Visit(newExpression.Arguments[i]);
- if (newArguments[i] == null)
- {
- return null;
- }
- _projectionMembers.Pop();
- }
+ _projectionMembers.Pop();
}
- return newExpression.Update(newArguments);
+ visitedExpression = MatchTypes(visitedExpression, expression.Type);
+
+ return memberAssignment.Update(visitedExpression);
}
///
@@ -398,24 +458,111 @@ protected override Expression VisitMemberInit(MemberInitExpression memberInitExp
/// 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.
///
- protected override MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment)
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
- if (_clientEval)
+ var @object = Visit(methodCallExpression.Object);
+ var arguments = new Expression[methodCallExpression.Arguments.Count];
+ for (var i = 0; i < methodCallExpression.Arguments.Count; i++)
+ {
+ var argument = methodCallExpression.Arguments[i];
+ arguments[i] = MatchTypes(Visit(argument), argument.Type);
+ }
+
+ Expression updatedMethodCallExpression = methodCallExpression.Update(
+ @object != null ? MatchTypes(@object, methodCallExpression.Object.Type) : @object,
+ arguments);
+
+ if (@object?.Type.IsNullableType() == true
+ && !methodCallExpression.Object.Type.IsNullableType())
{
- return memberAssignment.Update(Visit(memberAssignment.Expression));
+ var nullableReturnType = methodCallExpression.Type.MakeNullable();
+ if (!methodCallExpression.Type.IsNullableType())
+ {
+ updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType);
+ }
+
+ return Expression.Condition(
+ Expression.Equal(@object, Expression.Default(@object.Type)),
+ Expression.Constant(null, nullableReturnType),
+ updatedMethodCallExpression);
}
- var projectionMember = _projectionMembers.Peek().Append(memberAssignment.Member);
- _projectionMembers.Push(projectionMember);
+ return updatedMethodCallExpression;
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitNew(NewExpression newExpression)
+ {
+ Check.NotNull(newExpression, nameof(newExpression));
+
+ if (newExpression.Arguments.Count == 0)
+ {
+ return newExpression;
+ }
- var visitedExpression = Visit(memberAssignment.Expression);
- if (visitedExpression == null)
+ if (!_clientEval
+ && newExpression.Members == null)
{
return null;
}
- _projectionMembers.Pop();
- return memberAssignment.Update(visitedExpression);
+ var newArguments = new Expression[newExpression.Arguments.Count];
+ for (var i = 0; i < newArguments.Length; i++)
+ {
+ var argument = newExpression.Arguments[i];
+ Expression visitedArgument;
+ if (_clientEval)
+ {
+ visitedArgument = Visit(argument);
+ }
+ else
+ {
+ var projectionMember = _projectionMembers.Peek().Append(newExpression.Members[i]);
+ _projectionMembers.Push(projectionMember);
+ visitedArgument = Visit(argument);
+ if (visitedArgument == null)
+ {
+ return null;
+ }
+
+ _projectionMembers.Pop();
+ }
+
+ newArguments[i] = MatchTypes(visitedArgument, argument.Type);
+ }
+
+ return newExpression.Update(newArguments);
+ }
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
+ => newArrayExpression.Update(newArrayExpression.Expressions.Select(e => MatchTypes(Visit(e), e.Type)));
+
+ ///
+ /// 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.
+ ///
+ protected override Expression VisitUnary(UnaryExpression unaryExpression)
+ {
+ var operand = Visit(unaryExpression.Operand);
+
+ return (unaryExpression.NodeType == ExpressionType.Convert
+ || unaryExpression.NodeType == ExpressionType.ConvertChecked)
+ && unaryExpression.Type == operand.Type
+ ? operand
+ : unaryExpression.Update(MatchTypes(operand, unaryExpression.Operand.Type));
}
// TODO: Debugging
@@ -426,5 +573,26 @@ private void VerifySelectExpression(ProjectionBindingExpression projectionBindin
throw new InvalidOperationException(CoreStrings.QueryFailed(projectionBindingExpression.Print(), GetType().Name));
}
}
+
+ private static Expression MatchTypes(Expression expression, Type targetType)
+ {
+ if (targetType != expression.Type
+ && targetType.TryGetElementType(typeof(IQueryable<>)) == null)
+ {
+ if (targetType.MakeNullable() != expression.Type)
+ {
+ throw new InvalidFilterCriteriaException();
+ }
+
+ expression = Expression.Convert(expression, targetType);
+ }
+
+ return expression;
+ }
+
+#pragma warning disable IDE0052 // Remove unread private members
+ private static T GetParameterValue(QueryContext queryContext, string parameterName)
+#pragma warning restore IDE0052 // Remove unread private members
+ => (T)queryContext.ParameterValues[parameterName];
}
}
diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
index 5f47d60c19e..2b244e2838d 100644
--- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
+++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
@@ -80,7 +80,9 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType
for (var i = 0; i < concreteEntityTypes.Length; i++)
{
body = Condition(
- valueBufferParameter.CreateValueBufferReadValueExpression(typeof(bool), i, property: null),
+ Equal(
+ valueBufferParameter.CreateValueBufferReadValueExpression(typeof(bool?), i, property: null),
+ Constant(true, typeof(bool?))),
Constant(concreteEntityTypes[i], typeof(IEntityType)),
body);
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index 5a64572dd11..c7c437979f7 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -198,8 +198,12 @@ protected override ShapedQueryExpression TranslateAll(ShapedQueryExpression sour
}
translation = _sqlExpressionFactory.Exists(selectExpression, true);
- return source.Update(_sqlExpressionFactory.Select(translation),
- new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool)));
+
+ return source.Update(
+ _sqlExpressionFactory.Select(translation),
+ Expression.Convert(
+ new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)),
+ typeof(bool)));
}
///
@@ -223,8 +227,12 @@ protected override ShapedQueryExpression TranslateAny(ShapedQueryExpression sour
}
var translation = _sqlExpressionFactory.Exists(selectExpression, false);
- return source.Update(_sqlExpressionFactory.Select(translation),
- new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool)));
+
+ return source.Update(
+ _sqlExpressionFactory.Select(translation),
+ Expression.Convert(
+ new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)),
+ typeof(bool)));
}
///
@@ -298,8 +306,12 @@ protected override ShapedQueryExpression TranslateContains(ShapedQueryExpression
selectExpression.ApplyProjection();
translation = _sqlExpressionFactory.In(translation, selectExpression, false);
- return source.Update(_sqlExpressionFactory.Select(translation),
- new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool)));
+
+ return source.Update(
+ _sqlExpressionFactory.Select(translation),
+ Expression.Convert(
+ new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(bool?)),
+ typeof(bool)));
}
///
@@ -331,7 +343,11 @@ protected override ShapedQueryExpression TranslateCount(ShapedQueryExpression so
selectExpression.ClearOrdering();
selectExpression.ReplaceProjectionMapping(projectionMapping);
- return source.UpdateShaperExpression(new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int)));
+
+ return source.UpdateShaperExpression(
+ Expression.Convert(
+ new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)),
+ typeof(int)));
}
///
@@ -683,7 +699,11 @@ protected override ShapedQueryExpression TranslateLongCount(ShapedQueryExpressio
selectExpression.ClearOrdering();
selectExpression.ReplaceProjectionMapping(projectionMapping);
- return source.UpdateShaperExpression(new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long)));
+
+ return source.UpdateShaperExpression(
+ Expression.Convert(
+ new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long?)),
+ typeof(long)));
}
///
@@ -1553,9 +1573,9 @@ private ShapedQueryExpression AggregateResultShaper(
}
else
{
- // Sum case. Projection is always non-null. We read non-nullable value (0 if empty)
- shaper = new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), projection.Type);
- // Cast to nullable type if required
+ // Sum case. Projection is always non-null. We read nullable value.
+ shaper = new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), projection.Type.MakeNullable());
+
if (resultType != shaper.Type)
{
shaper = Expression.Convert(shaper, resultType);
diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
index 40e97ee6a14..022ce202439 100644
--- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs
@@ -413,11 +413,11 @@ protected override Expression VisitExtension(Expression extensionExpression)
var projection = _selectExpression.Projection[projectionIndex];
return CreateGetValueExpression(
- _dataReaderParameter,
- projectionIndex,
- IsNullableProjection(projection),
- projection.Expression.TypeMapping,
- projectionBindingExpression.Type);
+ _dataReaderParameter,
+ projectionIndex,
+ IsNullableProjection(projection),
+ projection.Expression.TypeMapping,
+ projectionBindingExpression.Type);
}
case ProjectionBindingExpression projectionBindingExpression
@@ -429,19 +429,20 @@ protected override Expression VisitExtension(Expression extensionExpression)
return accessor;
}
- var valueParameter = Expression.Parameter(projectionBindingExpression.Type);
- _variables.Add(valueParameter);
-
var projectionIndex = (int)GetProjectionIndex(projectionBindingExpression);
var projection = _selectExpression.Projection[projectionIndex];
+ var nullable = IsNullableProjection(projection);
+
+ var valueParameter = Expression.Parameter(projectionBindingExpression.Type);
+ _variables.Add(valueParameter);
_expressions.Add(Expression.Assign(valueParameter,
CreateGetValueExpression(
_dataReaderParameter,
projectionIndex,
- IsNullableProjection(projection),
+ nullable,
projection.Expression.TypeMapping,
- projectionBindingExpression.Type)));
+ valueParameter.Type)));
if (_containsCollectionMaterialization)
{
@@ -828,10 +829,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
: _materializationContextBindings[mappingParameter][property];
var projection = _selectExpression.Projection[projectionIndex];
+ var nullable = IsNullableProjection(projection);
+
+ Check.DebugAssert(!nullable || property != null || methodCallExpression.Type.IsNullableType(),
+ "For nullable reads the return type must be null unless property is specified.");
+
return CreateGetValueExpression(
_dataReaderParameter,
projectionIndex,
- IsNullableProjection(projection),
+ nullable,
projection.Expression.TypeMapping,
methodCallExpression.Type,
property);
@@ -907,6 +913,8 @@ private Expression CreateGetValueExpression(
Type clrType,
IPropertyBase property = null)
{
+ Check.DebugAssert(property != null || clrType.IsNullableType(), "Must read nullable value from database if property is not specified.");
+
var getMethod = typeMapping.GetDataReaderMethod();
Expression indexExpression = Expression.Constant(index);
@@ -923,35 +931,31 @@ Expression valueExpression
getMethod,
indexExpression);
- if (_readerColumns != null)
+ if (_readerColumns != null
+ && _readerColumns[index] == null)
{
- var columnType = valueExpression.Type;
+ var bufferedReaderLambdaExpression = valueExpression;
+ var columnType = bufferedReaderLambdaExpression.Type;
if (!columnType.IsValueType
|| !BufferedDataReader.IsSupportedValueType(columnType))
{
columnType = typeof(object);
- valueExpression = Expression.Convert(valueExpression, typeof(object));
+ bufferedReaderLambdaExpression = Expression.Convert(bufferedReaderLambdaExpression, columnType);
}
- if (_readerColumns[index] == null)
- {
- _readerColumns[index] = ReaderColumn.Create(
- columnType,
- nullable,
- _indexMapParameter != null ? ((ColumnExpression)_selectExpression.Projection[index].Expression).Name : null,
- Expression.Lambda(
- valueExpression,
- dbDataReader,
- _indexMapParameter ?? Expression.Parameter(typeof(int[]))).Compile());
- }
+ _readerColumns[index] = ReaderColumn.Create(
+ columnType,
+ nullable,
+ _indexMapParameter != null ? ((ColumnExpression)_selectExpression.Projection[index].Expression).Name : null,
+ Expression.Lambda(
+ bufferedReaderLambdaExpression,
+ dbDataReader,
+ _indexMapParameter ?? Expression.Parameter(typeof(int[]))).Compile());
if (getMethod.DeclaringType != typeof(DbDataReader))
{
- valueExpression
- = Expression.Call(
- dbDataReader,
- RelationalTypeMapping.GetDataReaderMethod(columnType),
- indexExpression);
+ valueExpression = Expression.Call(
+ dbDataReader, RelationalTypeMapping.GetDataReaderMethod(columnType), indexExpression);
}
}
@@ -977,35 +981,28 @@ Expression valueExpression
valueExpression = Expression.Convert(valueExpression, clrType);
}
- var exceptionParameter
- = Expression.Parameter(typeof(Exception), name: "e");
+ if (nullable)
+ {
+ valueExpression = Expression.Condition(
+ Expression.Call(dbDataReader, _isDbNullMethod, indexExpression),
+ Expression.Default(valueExpression.Type),
+ valueExpression);
+ }
if (_detailedErrorsEnabled)
{
- var catchBlock
- = Expression
- .Catch(
- exceptionParameter,
- Expression.Call(
- _throwReadValueExceptionMethod
- .MakeGenericMethod(valueExpression.Type),
- exceptionParameter,
- Expression.Call(
- dbDataReader,
- _getFieldValueMethod.MakeGenericMethod(typeof(object)),
- indexExpression),
- Expression.Constant(property, typeof(IPropertyBase))));
+ var exceptionParameter = Expression.Parameter(typeof(Exception), name: "e");
- valueExpression = Expression.TryCatch(valueExpression, catchBlock);
- }
+ var catchBlock = Expression.Catch(
+ exceptionParameter,
+ Expression.Call(
+ _throwReadValueExceptionMethod.MakeGenericMethod(valueExpression.Type),
+ exceptionParameter,
+ Expression.Call(dbDataReader, _getFieldValueMethod.MakeGenericMethod(typeof(object)), indexExpression),
+ Expression.Constant(valueExpression.Type.MakeNullable(nullable), typeof(Type)),
+ Expression.Constant(property, typeof(IPropertyBase))));
- if (nullable)
- {
- valueExpression
- = Expression.Condition(
- Expression.Call(dbDataReader, _isDbNullMethod, indexExpression),
- Expression.Default(valueExpression.Type),
- valueExpression);
+ valueExpression = Expression.TryCatch(valueExpression, catchBlock);
}
return valueExpression;
@@ -1013,9 +1010,8 @@ var catchBlock
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TValue ThrowReadValueException(
- Exception exception, object value, IPropertyBase property = null)
+ Exception exception, object value, Type expectedType, IPropertyBase property = null)
{
- var expectedType = typeof(TValue);
var actualType = value?.GetType();
string message;
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 3799d0c3f9f..6ccbaeb2e5f 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -552,6 +552,10 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method)
}
if (!(subqueryTranslation.ShaperExpression is ProjectionBindingExpression
+ || (subqueryTranslation.ShaperExpression is UnaryExpression unaryExpression
+ && unaryExpression.NodeType == ExpressionType.Convert
+ && unaryExpression.Type.MakeNullable() == unaryExpression.Operand.Type
+ && unaryExpression.Operand is ProjectionBindingExpression)
|| IsAggregateResultWithCustomShaper(methodCallExpression.Method)))
{
return null;
@@ -573,7 +577,17 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method)
return subquery.Projection[0].Expression;
}
- return new ScalarSubqueryExpression(subquery);
+ SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery);
+
+ if (subqueryTranslation.ResultCardinality == ResultCardinality.SingleOrDefault
+ && !subqueryTranslation.ShaperExpression.Type.IsNullableType())
+ {
+ scalarSubqueryExpression = _sqlExpressionFactory.Coalesce(
+ scalarSubqueryExpression,
+ (SqlExpression)Visit(subqueryTranslation.ShaperExpression.Type.GetDefaultValueConstant()));
+ }
+
+ return scalarSubqueryExpression;
}
SqlExpression sqlObject = null;
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index e8066a6e0e3..32136c75215 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -1199,23 +1199,32 @@ public Expression AddSingleProjection([NotNull] ShapedQueryExpression shapedQuer
if (!(innerExpression is EntityShaperExpression))
{
var sentinelExpression = innerSelectExpression.Limit;
+ var sentinelNullableType = sentinelExpression.Type.MakeNullable();
ProjectionBindingExpression dummyProjection;
if (innerSelectExpression.Projection.Any())
{
var index = innerSelectExpression.AddToProjection(sentinelExpression);
dummyProjection = new ProjectionBindingExpression(
- innerSelectExpression, index, sentinelExpression.Type);
+ innerSelectExpression, index, sentinelNullableType);
}
else
{
innerSelectExpression._projectionMapping[new ProjectionMember()] = sentinelExpression;
dummyProjection = new ProjectionBindingExpression(
- innerSelectExpression, new ProjectionMember(), sentinelExpression.Type);
+ innerSelectExpression, new ProjectionMember(), sentinelNullableType);
}
+ var defaultResult = shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault
+ ? (Expression)Default(shaperExpression.Type)
+ : Block(
+ Throw(New(
+ typeof(InvalidOperationException).GetConstructors().Single(ci => ci.GetParameters().Count() == 1),
+ Constant(RelationalStrings.SequenceContainsNoElements))),
+ Default(shaperExpression.Type));
+
shaperExpression = Condition(
- Equal(dummyProjection, Default(dummyProjection.Type)),
- Default(shaperExpression.Type),
+ Equal(dummyProjection, Default(sentinelNullableType)),
+ defaultResult,
shaperExpression);
}
@@ -2716,6 +2725,19 @@ private bool Equals(SelectExpression selectExpression)
return false;
}
+ if (_projection.Count != selectExpression._projection.Count)
+ {
+ return false;
+ }
+
+ for (var i = 0; i < _projection.Count; i++)
+ {
+ if (!_projection[i].Equals(selectExpression._projection[i]))
+ {
+ return false;
+ }
+ }
+
if (_projectionMapping.Count != selectExpression._projectionMapping.Count)
{
return false;
@@ -2889,6 +2911,11 @@ public override int GetHashCode()
var hash = new HashCode();
hash.Add(base.GetHashCode());
+ foreach (var projection in _projection)
+ {
+ hash.Add(projection);
+ }
+
foreach (var projectionMapping in _projectionMapping)
{
hash.Add(projectionMapping.Key);
diff --git a/src/EFCore/Extensions/ConventionModelExtensions.cs b/src/EFCore/Extensions/ConventionModelExtensions.cs
index 05686a1f247..5ca00de8c6c 100644
--- a/src/EFCore/Extensions/ConventionModelExtensions.cs
+++ b/src/EFCore/Extensions/ConventionModelExtensions.cs
@@ -300,6 +300,18 @@ public static string AddIgnored([NotNull] this IConventionModel model, [NotNull]
Check.NotNull(clrType, nameof(clrType)),
fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+ ///
+ /// Marks the given entity type as shared, indicating that when discovered matching entity types
+ /// should be configured as shared type entity type.
+ ///
+ /// The model to add the shared type to.
+ /// The type of the entity type that should be shared.
+ /// Indicates whether the configuration was specified using a data annotation.
+ public static Type AddShared([NotNull] this IConventionModel model, [NotNull] Type clrType, bool fromDataAnnotation = false)
+ => Check.NotNull((Model)model, nameof(model)).AddShared(
+ Check.NotNull(clrType, nameof(clrType)),
+ fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention);
+
///
/// Forces post-processing on the model such that it is ready for use by the runtime. This post
/// processing happens automatically when using ; this method allows it to be run
diff --git a/src/EFCore/Extensions/MutableModelExtensions.cs b/src/EFCore/Extensions/MutableModelExtensions.cs
index 2f77272b42b..a8a831cb11b 100644
--- a/src/EFCore/Extensions/MutableModelExtensions.cs
+++ b/src/EFCore/Extensions/MutableModelExtensions.cs
@@ -243,6 +243,16 @@ public static string RemoveOwned([NotNull] this IMutableModel model, [NotNull] T
=> Check.NotNull((Model)model, nameof(model)).RemoveOwned(
Check.NotNull(clrType, nameof(clrType)));
+ ///
+ /// Marks the given entity type as shared, indicating that when discovered matching entity types
+ /// should be configured as shared type entity type.
+ ///
+ /// The model to add the shared type to.
+ /// The type of the entity type that should be shared.
+ public static Type AddShared([NotNull] this IMutableModel model, [NotNull] Type clrType)
+ => Check.NotNull((Model)model, nameof(model)).AddShared(
+ Check.NotNull(clrType, nameof(clrType)), ConfigurationSource.Explicit);
+
///
/// Forces post-processing on the model such that it is ready for use by the runtime. This post
/// processing happens automatically when using ; this method allows it to be run
diff --git a/src/EFCore/Infrastructure/ExpressionExtensions.cs b/src/EFCore/Infrastructure/ExpressionExtensions.cs
index 888613fe5cf..82c41f8c910 100644
--- a/src/EFCore/Infrastructure/ExpressionExtensions.cs
+++ b/src/EFCore/Infrastructure/ExpressionExtensions.cs
@@ -319,7 +319,7 @@ private static TValue ValueBufferTryReadValue(
#pragma warning disable IDE0060 // Remove unused parameter
in ValueBuffer valueBuffer, int index, IPropertyBase property)
#pragma warning restore IDE0060 // Remove unused parameter
- => valueBuffer[index] is TValue value ? value : default;
+ => (TValue)valueBuffer[index];
///
///
diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs
index e8a11ceb312..18ab761bc0b 100644
--- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs
+++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs
@@ -83,37 +83,130 @@ public CollectionCollectionBuilder(
[EntityFrameworkInternal]
protected virtual InternalModelBuilder ModelBuilder => LeftEntityType.AsEntityType().Model.Builder;
+ ///
+ /// Configures the association entity type implementing the many-to-many relationship.
+ ///
+ /// The configuration of the association type.
+ /// The builder for the originating entity type so that multiple configuration calls can be chained.
+ public virtual EntityTypeBuilder UsingEntity(
+ [NotNull] Action configureAssociation)
+ {
+ Check.DebugAssert(LeftNavigation.AssociationEntityType != null, "LeftNavigation.AssociationEntityType is null");
+ Check.DebugAssert(RightNavigation.AssociationEntityType != null, "RightNavigation.AssociationEntityType is null");
+ Check.DebugAssert(LeftNavigation.AssociationEntityType == RightNavigation.AssociationEntityType,
+ "LeftNavigation.AssociationEntityType != RightNavigation.AssociationEntityType");
+
+ var associationEntityTypeBuilder = new EntityTypeBuilder(LeftNavigation.AssociationEntityType);
+ configureAssociation(associationEntityTypeBuilder);
+
+ return new EntityTypeBuilder(RightEntityType);
+ }
+
///
/// Configures the relationships to the entity types participating in the many-to-many relationship.
///
- /// The type of the join entity.
+ /// The CLR type of the join entity.
/// The configuration for the relationship to the right entity type.
/// The configuration for the relationship to the left entity type.
/// The builder for the association type.
public virtual EntityTypeBuilder UsingEntity(
- [NotNull] Type joinEntity,
+ [NotNull] Type joinEntityType,
[NotNull] Func configureRight,
[NotNull] Func configureLeft)
{
- if (((Model)LeftEntityType.Model).IsShared(joinEntity))
+ Check.NotNull(joinEntityType, nameof(joinEntityType));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+
+ var existingAssociationEntityType = (EntityType)
+ (LeftNavigation.ForeignKey?.DeclaringEntityType
+ ?? RightNavigation.ForeignKey?.DeclaringEntityType);
+ EntityType associationEntityType = null;
+ if (existingAssociationEntityType != null)
+ {
+ if (existingAssociationEntityType.ClrType == joinEntityType
+ && !existingAssociationEntityType.HasSharedClrType)
+ {
+ associationEntityType = existingAssociationEntityType;
+ }
+ else
+ {
+ ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
+ existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ }
+ }
+
+ if (associationEntityType == null)
{
- //TODO #9914 - when the generic version of "please use the shared-type entity type version of this API"
- // is available then update to use that.
- throw new InvalidOperationException(
- CoreStrings.DoNotUseUsingEntityOnSharedClrType(joinEntity.GetType().Name));
+ associationEntityType = ModelBuilder.Entity(joinEntityType, ConfigurationSource.Explicit).Metadata;
}
+ var entityTypeBuilder = new EntityTypeBuilder(associationEntityType);
+
+ var leftForeignKey = configureLeft(entityTypeBuilder).Metadata;
+ var rightForeignKey = configureRight(entityTypeBuilder).Metadata;
+
+ Using(rightForeignKey, leftForeignKey);
+
+ return entityTypeBuilder;
+ }
+
+ ///
+ /// Configures the relationships to the entity types participating in the many-to-many relationship.
+ ///
+ /// The name of the join entity.
+ /// The CLR type of the join entity.
+ /// The configuration for the relationship to the right entity type.
+ /// The configuration for the relationship to the left entity type.
+ /// The builder for the association type.
+ public virtual EntityTypeBuilder UsingEntity(
+ [NotNull] string joinEntityName,
+ [NotNull] Type joinEntityType,
+ [NotNull] Func configureRight,
+ [NotNull] Func configureLeft)
+ {
+ Check.NotEmpty(joinEntityName, nameof(joinEntityName));
+ Check.NotNull(joinEntityType, nameof(joinEntityType));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+
var existingAssociationEntityType = (EntityType)
(LeftNavigation.ForeignKey?.DeclaringEntityType
?? RightNavigation.ForeignKey?.DeclaringEntityType);
+ EntityType associationEntityType = null;
if (existingAssociationEntityType != null)
{
- ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
- existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ if (existingAssociationEntityType.ClrType == joinEntityType
+ && string.Equals(existingAssociationEntityType.Name, joinEntityName, StringComparison.Ordinal))
+ {
+ associationEntityType = existingAssociationEntityType;
+ }
+ else
+ {
+ ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
+ existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ }
}
- var entityTypeBuilder = new EntityTypeBuilder(
- ModelBuilder.Entity(joinEntity, ConfigurationSource.Explicit).Metadata);
+ if (associationEntityType == null)
+ {
+ var existingEntityType = ModelBuilder.Metadata.FindEntityType(joinEntityName);
+ if (existingEntityType?.ClrType == joinEntityType)
+ {
+ associationEntityType = existingEntityType;
+ }
+ else
+ {
+ if (!ModelBuilder.Metadata.IsShared(joinEntityType))
+ {
+ throw new InvalidOperationException(CoreStrings.TypeNotMarkedAsShared(joinEntityType.DisplayName()));
+ }
+
+ associationEntityType = ModelBuilder.SharedEntity(joinEntityName, joinEntityType, ConfigurationSource.Explicit).Metadata;
+ }
+ }
+
+ var entityTypeBuilder = new EntityTypeBuilder(associationEntityType);
var leftForeignKey = configureLeft(entityTypeBuilder).Metadata;
var rightForeignKey = configureRight(entityTypeBuilder).Metadata;
@@ -126,18 +219,51 @@ public virtual EntityTypeBuilder UsingEntity(
///
/// Configures the relationships to the entity types participating in the many-to-many relationship.
///
- /// The type of the join entity.
+ /// The CLR type of the join entity.
/// The configuration for the relationship to the right entity type.
/// The configuration for the relationship to the left entity type.
/// The configuration of the association type.
/// The builder for the originating entity type so that multiple configuration calls can be chained.
public virtual EntityTypeBuilder UsingEntity(
- [NotNull] Type joinEntity,
+ [NotNull] Type joinEntityType,
[NotNull] Func configureRight,
[NotNull] Func configureLeft,
[NotNull] Action configureAssociation)
{
- var entityTypeBuilder = UsingEntity(joinEntity, configureRight, configureLeft);
+ Check.NotNull(joinEntityType, nameof(joinEntityType));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+ Check.NotNull(configureAssociation, nameof(configureAssociation));
+
+ var entityTypeBuilder = UsingEntity(joinEntityType, configureRight, configureLeft);
+ configureAssociation(entityTypeBuilder);
+
+ return new EntityTypeBuilder(RightEntityType);
+ }
+
+ ///
+ /// Configures the relationships to the entity types participating in the many-to-many relationship.
+ ///
+ /// The name of the join entity.
+ /// The CLR type of the join entity.
+ /// The configuration for the relationship to the right entity type.
+ /// The configuration for the relationship to the left entity type.
+ /// The configuration of the association type.
+ /// The builder for the originating entity type so that multiple configuration calls can be chained.
+ public virtual EntityTypeBuilder UsingEntity(
+ [NotNull] string joinEntityName,
+ [NotNull] Type joinEntityType,
+ [NotNull] Func configureRight,
+ [NotNull] Func configureLeft,
+ [NotNull] Action configureAssociation)
+ {
+ Check.NotEmpty(joinEntityName, nameof(joinEntityName));
+ Check.NotNull(joinEntityType, nameof(joinEntityType));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+ Check.NotNull(configureAssociation, nameof(configureAssociation));
+
+ var entityTypeBuilder = UsingEntity(joinEntityName, joinEntityType, configureRight, configureLeft);
configureAssociation(entityTypeBuilder);
return new EntityTypeBuilder(RightEntityType);
diff --git a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs
index b785a0efe2a..f4c71151464 100644
--- a/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs
@@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.Metadata.Builders
{
@@ -40,6 +41,25 @@ public CollectionCollectionBuilder(
{
}
+ ///
+ /// Configures the association entity type implementing the many-to-many relationship.
+ ///
+ /// The configuration of the association type.
+ /// The builder for the originating entity type so that multiple configuration calls can be chained.
+ public new virtual EntityTypeBuilder UsingEntity(
+ [NotNull] Action configureAssociation)
+ {
+ Check.DebugAssert(LeftNavigation.AssociationEntityType != null, "LeftNavigation.AssociationEntityType is null");
+ Check.DebugAssert(RightNavigation.AssociationEntityType != null, "RightNavigation.AssociationEntityType is null");
+ Check.DebugAssert(LeftNavigation.AssociationEntityType == RightNavigation.AssociationEntityType,
+ "LeftNavigation.AssociationEntityType != RightNavigation.AssociationEntityType");
+
+ var associationEntityTypeBuilder = new EntityTypeBuilder(LeftNavigation.AssociationEntityType);
+ configureAssociation(associationEntityTypeBuilder);
+
+ return new EntityTypeBuilder(RightEntityType);
+ }
+
///
/// Configures the relationships to the entity types participating in the many-to-many relationship.
///
@@ -52,25 +72,97 @@ public virtual EntityTypeBuilder UsingEntity, ReferenceCollectionBuilder> configureLeft)
where TAssociationEntity : class
{
- if (((Model)LeftEntityType.Model).IsShared(typeof(TAssociationEntity)))
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+
+ var existingAssociationEntityType = (EntityType)
+ (LeftNavigation.ForeignKey?.DeclaringEntityType
+ ?? RightNavigation.ForeignKey?.DeclaringEntityType);
+ EntityType associationEntityType = null;
+ if (existingAssociationEntityType != null)
{
- //TODO #9914 - when the generic version of "please use the shared-type entity type version of this API"
- // is available then update to use that.
- throw new InvalidOperationException(
- CoreStrings.DoNotUseUsingEntityOnSharedClrType(typeof(TAssociationEntity).Name));
+ if (existingAssociationEntityType.ClrType == typeof(TAssociationEntity)
+ && !existingAssociationEntityType.HasSharedClrType)
+ {
+ associationEntityType = existingAssociationEntityType;
+ }
+ else
+ {
+ ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
+ existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ }
}
+ if (associationEntityType == null)
+ {
+ associationEntityType = ModelBuilder.Entity(typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata;
+ }
+
+ var entityTypeBuilder = new EntityTypeBuilder(associationEntityType);
+
+ var leftForeignKey = configureLeft(entityTypeBuilder).Metadata;
+ var rightForeignKey = configureRight(entityTypeBuilder).Metadata;
+
+ Using(rightForeignKey, leftForeignKey);
+
+ return entityTypeBuilder;
+ }
+
+ ///
+ /// Configures the relationships to the entity types participating in the many-to-many relationship.
+ ///
+ /// The name of the association entity.
+ /// The configuration for the relationship to the right entity type.
+ /// The configuration for the relationship to the left entity type.
+ /// The type of the association entity.
+ /// The builder for the association type.
+ public virtual EntityTypeBuilder UsingEntity(
+ [NotNull] string joinEntityName,
+ [NotNull] Func, ReferenceCollectionBuilder> configureRight,
+ [NotNull] Func, ReferenceCollectionBuilder> configureLeft)
+ where TAssociationEntity : class
+ {
+ Check.NotEmpty(joinEntityName, nameof(joinEntityName));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+
var existingAssociationEntityType = (EntityType)
(LeftNavigation.ForeignKey?.DeclaringEntityType
?? RightNavigation.ForeignKey?.DeclaringEntityType);
+ EntityType associationEntityType = null;
if (existingAssociationEntityType != null)
{
- ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
- existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ if (existingAssociationEntityType.ClrType == typeof(TAssociationEntity)
+ && string.Equals(existingAssociationEntityType.Name, joinEntityName, StringComparison.Ordinal))
+ {
+ associationEntityType = existingAssociationEntityType;
+ }
+ else
+ {
+ ModelBuilder.RemoveAssociationEntityIfCreatedImplicitly(
+ existingAssociationEntityType, removeSkipNavigations: false, ConfigurationSource.Explicit);
+ }
+ }
+
+ if (associationEntityType == null)
+ {
+ var existingEntityType = ModelBuilder.Metadata.FindEntityType(joinEntityName);
+ if (existingEntityType?.ClrType == typeof(TAssociationEntity))
+ {
+ associationEntityType = existingEntityType;
+ }
+ else
+ {
+ if (!ModelBuilder.Metadata.IsShared(typeof(TAssociationEntity)))
+ {
+ throw new InvalidOperationException(CoreStrings.TypeNotMarkedAsShared(typeof(TAssociationEntity).DisplayName()));
+ }
+
+ associationEntityType = ModelBuilder.SharedEntity(joinEntityName, typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata;
+ }
}
- var entityTypeBuilder = new EntityTypeBuilder(
- ModelBuilder.Entity(typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata);
+ var entityTypeBuilder = new EntityTypeBuilder(associationEntityType);
var leftForeignKey = configureLeft(entityTypeBuilder).Metadata;
var rightForeignKey = configureRight(entityTypeBuilder).Metadata;
@@ -94,7 +186,37 @@ public virtual EntityTypeBuilder UsingEntity(
[NotNull] Action> configureAssociation)
where TAssociationEntity : class
{
- var entityTypeBuilder = UsingEntity(configureRight, configureLeft);
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+
+ var entityTypeBuilder = UsingEntity(configureRight, configureLeft);
+ configureAssociation(entityTypeBuilder);
+
+ return new EntityTypeBuilder(RightEntityType);
+ }
+
+ ///
+ /// Configures the relationships to the entity types participating in the many-to-many relationship.
+ ///
+ /// The name of the association entity.
+ /// The configuration for the relationship to the right entity type.
+ /// The configuration for the relationship to the left entity type.
+ /// The configuration of the association type.
+ /// The type of the association entity.
+ /// The builder for the originating entity type so that multiple configuration calls can be chained.
+ public virtual EntityTypeBuilder UsingEntity(
+ [NotNull] string joinEntityName,
+ [NotNull] Func, ReferenceCollectionBuilder> configureRight,
+ [NotNull] Func, ReferenceCollectionBuilder> configureLeft,
+ [NotNull] Action> configureAssociation)
+ where TAssociationEntity : class
+ {
+ Check.NotEmpty(joinEntityName, nameof(joinEntityName));
+ Check.NotNull(configureRight, nameof(configureRight));
+ Check.NotNull(configureLeft, nameof(configureLeft));
+ Check.NotNull(configureAssociation, nameof(configureAssociation));
+
+ var entityTypeBuilder = UsingEntity(joinEntityName, configureRight, configureLeft);
configureAssociation(entityTypeBuilder);
return new EntityTypeBuilder(RightEntityType);
diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs
index 7ccb3d02088..8e3ac1a2cff 100644
--- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs
+++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs
@@ -376,7 +376,7 @@ public virtual EntityTypeBuilder OwnsOne(
Check.NotEmpty(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName));
+ buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName));
return this;
}
@@ -412,7 +412,7 @@ public virtual EntityTypeBuilder OwnsOne(
Check.NotEmpty(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName));
+ buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName));
return this;
}
@@ -523,7 +523,7 @@ public virtual EntityTypeBuilder OwnsMany(
Check.NotEmpty(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName));
+ buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName));
return this;
}
@@ -559,7 +559,7 @@ public virtual EntityTypeBuilder OwnsMany(
Check.NotEmpty(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName));
+ buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName));
return this;
}
diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs
index 4896d922d0f..5c9ba6df4bf 100644
--- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs
@@ -347,8 +347,7 @@ public virtual OwnedNavigationBuilder OwnsOne OwnsOne(
[NotNull] Expression> navigationExpression)
where TRelatedEntity : class
- => OwnsOneBuilder(
- new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
+ => OwnsOneBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
///
///
@@ -381,7 +380,7 @@ public virtual EntityTypeBuilder OwnsOne(
Check.NotNull(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationName)));
+ buildAction(OwnsOneBuilder(new MemberIdentity(navigationName)));
return this;
}
@@ -417,7 +416,7 @@ public virtual EntityTypeBuilder OwnsOne(
Check.NotNull(navigationExpression, nameof(navigationExpression));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
+ buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
return this;
}
@@ -427,9 +426,8 @@ private OwnedNavigationBuilder OwnsOneBuilder OwnsMany OwnsMany(
[NotNull] Expression>> navigationExpression)
where TRelatedEntity : class
- => OwnsManyBuilder(
- new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
+ => OwnsManyBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
///
///
@@ -524,10 +521,10 @@ public virtual EntityTypeBuilder OwnsMany(
[NotNull] Action> buildAction)
where TRelatedEntity : class
{
- Check.NotNull(navigationName, nameof(navigationName));
+ Check.NotEmpty(navigationName, nameof(navigationName));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationName)));
+ buildAction(OwnsManyBuilder(new MemberIdentity(navigationName)));
return this;
}
@@ -563,7 +560,7 @@ public virtual EntityTypeBuilder OwnsMany(
Check.NotNull(navigationExpression, nameof(navigationExpression));
Check.NotNull(buildAction, nameof(buildAction));
- buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
+ buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
return this;
}
@@ -573,9 +570,8 @@ private OwnedNavigationBuilder OwnsManyBuilder
IConventionEntityTypeBuilder Entity([NotNull] string name, bool? shouldBeOwned = false, bool fromDataAnnotation = false);
+ ///
+ ///
+ /// Returns an object that can be used to configure a given shared type entity type in the model.
+ ///
+ ///
+ /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR
+ /// type will be added to the model as shared type entity type.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// The name of the entity type to be configured.
+ /// The type of the entity type to be configured.
+ ///
+ /// if the entity type should be owned,
+ /// if the entity type should not be owned
+ ///
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// An object that can be used to configure the entity type if the entity type was added or already part of the model,
+ /// otherwise.
+ ///
+ IConventionEntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type type, bool? shouldBeOwned = false, bool fromDataAnnotation = false);
+
///
/// Returns an object that can be used to configure a given entity type in the model.
/// If an entity type with the provided type is not already part of the model,
diff --git a/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs
new file mode 100644
index 00000000000..7674d56ac2a
--- /dev/null
+++ b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs
@@ -0,0 +1,13 @@
+// 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.
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ /// This interface is typically used by database providers (and other extensions). It is generally
+ /// not used in application code.
+ ///
+ public interface IConventionSharedEntityTypeBuilder
+ {
+ }
+}
diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs
index b35f18ae957..d56be9f980a 100644
--- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs
+++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs
@@ -397,7 +397,7 @@ public virtual OwnedNavigationBuilder OwnsOne(
using (DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName));
+ buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName));
return this;
}
}
@@ -437,7 +437,7 @@ public virtual OwnedNavigationBuilder OwnsOne(
using (DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName));
+ buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName));
return this;
}
}
@@ -551,7 +551,7 @@ public virtual OwnedNavigationBuilder OwnsMany(
using (DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName));
+ buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName));
return this;
}
}
@@ -590,7 +590,7 @@ public virtual OwnedNavigationBuilder OwnsMany(
using (DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName));
+ buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName));
return this;
}
}
diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs
index 33b4177954a..22d4f130972 100644
--- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs
+++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs
@@ -284,8 +284,7 @@ public virtual OwnedNavigationBuilder Own
[NotNull] Expression> navigationExpression)
where TNewDependentEntity : class
=> OwnsOneBuilder(
- new MemberIdentity(
- Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
+ new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess()));
///
///
@@ -319,7 +318,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationName)));
+ buildAction(OwnsOneBuilder(new MemberIdentity(navigationName)));
return this;
}
@@ -356,7 +355,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationExpression.GetMemberAccess())));
+ buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
return this;
}
@@ -367,10 +366,8 @@ private OwnedNavigationBuilder OwnsOneBui
InternalForeignKeyBuilder relationship;
using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- relationship = navigation.MemberInfo == null
- ? DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation.Name, ConfigurationSource.Explicit)
- : DependentEntityType.Builder.HasOwnership(
- typeof(TNewDependentEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit);
+ relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation, ConfigurationSource.Explicit);
+
relationship.IsUnique(true, ConfigurationSource.Explicit);
relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder;
}
@@ -470,7 +467,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationName)));
+ buildAction(OwnsManyBuilder(new MemberIdentity(navigationName)));
return this;
}
}
@@ -509,7 +506,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationExpression.GetMemberAccess())));
+ buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess())));
return this;
}
}
@@ -520,10 +517,8 @@ private OwnedNavigationBuilder OwnsManyBuil
InternalForeignKeyBuilder relationship;
using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions())
{
- relationship = navigation.MemberInfo == null
- ? DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation.Name, ConfigurationSource.Explicit)
- : DependentEntityType.Builder.HasOwnership(
- typeof(TNewRelatedEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit);
+ relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation, ConfigurationSource.Explicit);
+
relationship.IsUnique(false, ConfigurationSource.Explicit);
relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder;
}
diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs
new file mode 100644
index 00000000000..13fe5a83bbf
--- /dev/null
+++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs
@@ -0,0 +1,54 @@
+// 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.ComponentModel;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ ///
+ /// Instances of this class are returned from methods when using the API
+ /// and it is not designed to be directly constructed in your application code.
+ ///
+ ///
+ public class SharedEntityTypeBuilder
+ {
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public SharedEntityTypeBuilder()
+ {
+ }
+
+ #region Hidden System.Object members
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override string ToString() => base.ToString();
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// if the specified object is equal to the current object; otherwise, .
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override bool Equals(object obj) => base.Equals(obj);
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override int GetHashCode() => base.GetHashCode();
+
+ #endregion
+ }
+}
diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs
new file mode 100644
index 00000000000..8abb3b920a5
--- /dev/null
+++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs
@@ -0,0 +1,29 @@
+// 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 Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ ///
+ /// Instances of this class are returned from methods when using the API
+ /// and it is not designed to be directly constructed in your application code.
+ ///
+ ///
+ /// The entity type being configured.
+ // ReSharper disable once UnusedTypeParameter
+ public class SharedEntityTypeBuilder : SharedEntityTypeBuilder
+ {
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public SharedEntityTypeBuilder()
+ {
+ }
+ }
+}
diff --git a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs
index 02026872d57..6e766f8dd67 100644
--- a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs
+++ b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs
@@ -106,12 +106,9 @@ private void CreateAssociationEntityType(
inverseEntityType.ShortName()),
otherIdentifiers,
int.MaxValue);
- //TODO #9914 - when the shared-type entity type version of model.Entity() is available call that instead
- var associationEntityTypeBuilder =
- model.AddEntityType(
- associationEntityTypeName,
- Model.DefaultPropertyBagType,
- ConfigurationSource.Convention).Builder;
+
+ var associationEntityTypeBuilder = model.Builder.SharedEntity(
+ associationEntityTypeName, Model.DefaultPropertyBagType, ConfigurationSource.Convention);
// Create left and right foreign keys from the outer entity types to
// the association entity type and configure the skip navigations.
diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs
index 4acdc409ddf..c4894254bca 100644
--- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs
+++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs
@@ -2954,8 +2954,7 @@ public virtual InternalForeignKeyBuilder HasOwnership(
[NotNull] string navigationName,
ConfigurationSource configurationSource)
=> HasOwnership(
- new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName),
- inverse: null, configurationSource);
+ new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName), inverse: null, configurationSource);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -2968,8 +2967,7 @@ public virtual InternalForeignKeyBuilder HasOwnership(
[NotNull] string navigationName,
ConfigurationSource configurationSource)
=> HasOwnership(
- new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName),
- inverse: null, configurationSource);
+ new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName), inverse: null, configurationSource);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -2982,8 +2980,20 @@ public virtual InternalForeignKeyBuilder HasOwnership(
[NotNull] MemberInfo navigationMember,
ConfigurationSource configurationSource)
=> HasOwnership(
- new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember),
- inverse: null, configurationSource);
+ new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember), inverse: null, configurationSource);
+
+ ///
+ /// 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.
+ ///
+ public virtual InternalForeignKeyBuilder HasOwnership(
+ [NotNull] Type targetEntityType,
+ MemberIdentity navigation,
+ ConfigurationSource configurationSource)
+ => HasOwnership(
+ new TypeIdentity(targetEntityType, Metadata.Model), navigation, inverse: null, configurationSource);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -3035,6 +3045,7 @@ private InternalForeignKeyBuilder HasOwnership(
if (existingNavigation.TargetEntityType.Name == targetEntityType.Name)
{
var existingOwnedEntityType = existingNavigation.ForeignKey.DeclaringEntityType;
+ // Upgrade configurationSource for existing entity type
if (existingOwnedEntityType.HasDefiningNavigation())
{
if (targetEntityType.Type != null)
@@ -4246,7 +4257,8 @@ private bool CanAddDiscriminatorProperty(
///
IConventionEntityType IConventionEntityTypeBuilder.Metadata
{
- [DebuggerStepThrough] get => Metadata;
+ [DebuggerStepThrough]
+ get => Metadata;
}
///
diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs
index 95fbf275a8c..54706f1c342 100644
--- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs
+++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs
@@ -48,7 +48,17 @@ public InternalModelBuilder([NotNull] Model metadata)
///
public virtual InternalEntityTypeBuilder Entity(
[NotNull] string name, ConfigurationSource configurationSource, bool? shouldBeOwned = false)
- => Entity(new TypeIdentity(name), configurationSource, shouldBeOwned);
+ => Entity(new TypeIdentity(name), null, configurationSource, shouldBeOwned);
+
+ ///
+ /// 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.
+ ///
+ public virtual InternalEntityTypeBuilder SharedEntity(
+ [NotNull] string name, [NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false)
+ => Entity(new TypeIdentity(name), Check.NotNull(type, nameof(type)), configurationSource, shouldBeOwned);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -58,10 +68,10 @@ public virtual InternalEntityTypeBuilder Entity(
///
public virtual InternalEntityTypeBuilder Entity(
[NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false)
- => Entity(new TypeIdentity(type, Metadata), configurationSource, shouldBeOwned);
+ => Entity(new TypeIdentity(type, Metadata), null, configurationSource, shouldBeOwned);
private InternalEntityTypeBuilder Entity(
- in TypeIdentity type, ConfigurationSource configurationSource, bool? shouldBeOwned)
+ in TypeIdentity type, Type sharedTypeClrType, ConfigurationSource configurationSource, bool? shouldBeOwned)
{
if (IsIgnored(type, configurationSource))
{
@@ -69,14 +79,35 @@ private InternalEntityTypeBuilder Entity(
}
var clrType = type.Type;
- var entityType = clrType == null
- ? Metadata.FindEntityType(type.Name)
- : Metadata.FindEntityType(clrType);
+ EntityType entityType;
+ if (clrType != null)
+ {
+ if (Metadata.IsShared(clrType))
+ {
+ return configurationSource == ConfigurationSource.Explicit
+ ? throw new InvalidOperationException(CoreStrings.ClashingSharedType(clrType.DisplayName()))
+ : (InternalEntityTypeBuilder)null;
+ }
+
+ entityType = Metadata.FindEntityType(clrType);
+ }
+ else
+ {
+ if (sharedTypeClrType != null && Metadata.FindEntityType(Metadata.GetDisplayName(sharedTypeClrType)) != null)
+ {
+ return configurationSource == ConfigurationSource.Explicit
+ ? throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(type.Name))
+ : (InternalEntityTypeBuilder)null;
+ }
+
+ entityType = Metadata.FindEntityType(type.Name);
+ }
if (shouldBeOwned == false
- && (ShouldBeOwnedType(type)
- || entityType != null && entityType.IsOwned()))
+ && (ShouldBeOwnedType(type) // Marked in model as owned
+ || entityType != null && entityType.IsOwned())) // Created using Owns* API
{
+ // We always throw as configuring a type as owned is always comes from user (through Explicit/DataAnnotation)
throw new InvalidOperationException(
CoreStrings.ClashingOwnedEntityType(
clrType == null ? type.Name : clrType.ShortDisplayName()));
@@ -106,14 +137,24 @@ private InternalEntityTypeBuilder Entity(
if (entityType != null)
{
+ if (sharedTypeClrType != null)
+ {
+ if (entityType.ClrType != sharedTypeClrType)
+ {
+ throw new InvalidOperationException(CoreStrings.ClashingMismatchedSharedType(type.Name));
+ }
+ }
+
entityType.UpdateConfigurationSource(configurationSource);
return entityType.Builder;
}
Metadata.RemoveIgnored(type.Name);
- entityType = clrType == null
- ? Metadata.AddEntityType(type.Name, configurationSource)
- : Metadata.AddEntityType(clrType, configurationSource);
+ entityType = clrType != null
+ ? Metadata.AddEntityType(clrType, configurationSource)
+ : sharedTypeClrType != null
+ ? Metadata.AddEntityType(type.Name, sharedTypeClrType, configurationSource)
+ : Metadata.AddEntityType(type.Name, configurationSource);
return entityType?.Builder;
}
@@ -307,6 +348,34 @@ public virtual IConventionOwnedEntityTypeBuilder Owned(
private bool ShouldBeOwnedType(in TypeIdentity type)
=> type.Type != null && Metadata.IsOwned(type.Type);
+ ///
+ /// 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.
+ ///
+ public virtual IConventionSharedEntityTypeBuilder SharedEntity(
+ [NotNull] Type type, ConfigurationSource configurationSource)
+ {
+ if (IsIgnored(type, configurationSource))
+ {
+ return null;
+ }
+
+ Metadata.RemoveIgnored(type);
+
+ foreach (var entityType in Metadata.GetEntityTypes()
+ .Where(et => !et.HasSharedClrType && et.ClrType == type && configurationSource.Overrides(et.GetConfigurationSource()))
+ .ToList())
+ {
+ HasNoEntityType(entityType, configurationSource);
+ }
+
+ Metadata.AddShared(type);
+
+ return new InternalSharedEntityTypeBuilder();
+ }
+
///
/// 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
@@ -560,7 +629,8 @@ public virtual bool CanSetPropertyAccessMode(
///
IConventionModel IConventionModelBuilder.Metadata
{
- [DebuggerStepThrough] get => Metadata;
+ [DebuggerStepThrough]
+ get => Metadata;
}
///
@@ -573,6 +643,16 @@ IConventionModel IConventionModelBuilder.Metadata
IConventionEntityTypeBuilder IConventionModelBuilder.Entity(string name, bool? shouldBeOwned, bool fromDataAnnotation)
=> Entity(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned);
+ ///
+ /// 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.
+ ///
+ [DebuggerStepThrough]
+ IConventionEntityTypeBuilder IConventionModelBuilder.SharedEntity(string name, Type type, bool? shouldBeOwned, bool fromDataAnnotation)
+ => SharedEntity(name, type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned);
+
///
/// 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
diff --git a/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs
new file mode 100644
index 00000000000..849619e4f46
--- /dev/null
+++ b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs
@@ -0,0 +1,17 @@
+// 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 Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Internal
+{
+ ///
+ /// 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.
+ ///
+ public class InternalSharedEntityTypeBuilder : IConventionSharedEntityTypeBuilder
+ {
+ }
+}
diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs
index 922e797674d..beec03812b4 100644
--- a/src/EFCore/Metadata/Internal/Model.cs
+++ b/src/EFCore/Metadata/Internal/Model.cs
@@ -58,7 +58,7 @@ private readonly SortedDictionary> _entityTypesWit
private readonly Dictionary _ignoredTypeNames
= new Dictionary(StringComparer.Ordinal);
- private readonly HashSet _sharedEntityClrTypes = new HashSet();
+ private readonly Dictionary _sharedEntityClrTypes = new Dictionary();
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -235,9 +235,17 @@ private EntityType AddEntityType(EntityType entityType)
throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(entityType.DisplayName()));
}
- _sharedEntityClrTypes.Add(entityType.ClrType);
+ if (_sharedEntityClrTypes.TryGetValue(entityType.ClrType, out var existingConfigurationSource))
+ {
+ _sharedEntityClrTypes[entityType.ClrType] = entityType.GetConfigurationSource().Max(existingConfigurationSource);
+ }
+ else
+ {
+ _sharedEntityClrTypes.Add(entityType.ClrType, entityType.GetConfigurationSource());
+ }
}
- else if (_sharedEntityClrTypes.Contains(entityType.ClrType))
+ else if (entityType.ClrType != null
+ && _sharedEntityClrTypes.ContainsKey(entityType.ClrType))
{
throw new InvalidOperationException(CoreStrings.ClashingSharedType(entityType.DisplayName()));
}
@@ -772,7 +780,7 @@ public virtual bool IsIgnored([NotNull] Type type)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual bool IsShared([NotNull] Type type)
- => _sharedEntityClrTypes.Contains(type);
+ => _sharedEntityClrTypes.ContainsKey(type);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -877,6 +885,31 @@ public virtual string RemoveOwned([NotNull] Type clrType)
return ownedTypes.Remove(name) ? name : null;
}
+ ///
+ /// 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.
+ ///
+ public virtual Type AddShared([NotNull] Type clrType, ConfigurationSource configurationSource)
+ {
+ if (_entityTypes.Any(et => !et.Value.HasSharedClrType && et.Value.ClrType == clrType))
+ {
+ throw new InvalidOperationException(CoreStrings.CannotMarkShared(clrType.ShortDisplayName()));
+ }
+
+ if (_sharedEntityClrTypes.TryGetValue(clrType, out var existingConfigurationSource))
+ {
+ _sharedEntityClrTypes[clrType] = configurationSource.Max(existingConfigurationSource);
+ }
+ else
+ {
+ _sharedEntityClrTypes.Add(clrType, configurationSource);
+ }
+
+ return clrType;
+ }
+
///
/// 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
diff --git a/src/EFCore/ModelBuilder.cs b/src/EFCore/ModelBuilder.cs
index 111e7e0be7a..4725a3c4246 100644
--- a/src/EFCore/ModelBuilder.cs
+++ b/src/EFCore/ModelBuilder.cs
@@ -115,6 +115,30 @@ public virtual EntityTypeBuilder Entity()
where TEntity : class
=> new EntityTypeBuilder(Builder.Entity(typeof(TEntity), ConfigurationSource.Explicit).Metadata);
+ ///
+ ///
+ /// Returns an object that can be used to configure a given shared type entity type in the model.
+ ///
+ ///
+ /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR
+ /// type will be added to the model as shared type entity type.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// The CLR type of the entity type to be configured.
+ /// The name of the entity type to be configured.
+ /// An object that can be used to configure the entity type.
+ public virtual EntityTypeBuilder SharedEntity([NotNull] string name)
+ where TEntity : class
+ {
+ Check.NotEmpty(name, nameof(name));
+
+ return new EntityTypeBuilder(Builder.SharedEntity(name, typeof(TEntity), ConfigurationSource.Explicit).Metadata);
+ }
+
///
/// Returns an object that can be used to configure a given entity type in the model.
/// If the entity type is not already part of the model, it will be added to the model.
@@ -142,6 +166,30 @@ public virtual EntityTypeBuilder Entity([NotNull] string name)
return new EntityTypeBuilder(Builder.Entity(name, ConfigurationSource.Explicit).Metadata);
}
+ ///
+ ///
+ /// Returns an object that can be used to configure a given shared type entity type in the model.
+ ///
+ ///
+ /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR
+ /// type will be added to the model as shared type entity type.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// The name of the entity type to be configured.
+ /// The CLR type of the entity type to be configured.
+ /// An object that can be used to configure the entity type.
+ public virtual EntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType)
+ {
+ Check.NotEmpty(name, nameof(name));
+ Check.NotNull(clrType, nameof(clrType));
+
+ return new EntityTypeBuilder(Builder.SharedEntity(name, clrType, ConfigurationSource.Explicit).Metadata);
+ }
+
///
///
/// Performs configuration of a given entity type in the model. If the entity type is not already part
@@ -168,6 +216,41 @@ public virtual ModelBuilder Entity([NotNull] Action
+ ///
+ /// Returns an object that can be used to configure a given shared type entity type in the model.
+ ///
+ ///
+ /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR
+ /// type will be added to the model as shared type entity type.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// This overload allows configuration of the entity type to be done inline in the method call rather
+ /// than being chained after a call to . This allows additional
+ /// configuration at the model level to be chained after configuration for the entity type.
+ ///
+ ///
+ /// The CLR type of the entity type to be configured.
+ /// The name of the entity type to be configured.
+ /// An action that performs configuration of the entity type.
+ ///
+ /// The same instance so that additional configuration calls can be chained.
+ ///
+ public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Action> buildAction)
+ where TEntity : class
+ {
+ Check.NotEmpty(name, nameof(name));
+ Check.NotNull(buildAction, nameof(buildAction));
+
+ buildAction(SharedEntity(name));
+
+ return this;
+ }
+
///
///
/// Performs configuration of a given entity type in the model. If the entity type is not already part
@@ -221,11 +304,46 @@ public virtual ModelBuilder Entity([NotNull] string name, [NotNull] Action
+ ///
+ /// Returns an object that can be used to configure a given shared type entity type in the model.
+ ///
+ ///
+ /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR
+ /// type will be added to the model as shared type entity type.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// This overload allows configuration of the entity type to be done in line in the method call rather
+ /// than being chained after a call to . This allows additional
+ /// configuration at the model level to be chained after configuration for the entity type.
+ ///
+ ///
+ /// The name of the entity type to be configured.
+ /// The CLR type of the entity type to be configured.
+ /// An action that performs configuration of the entity type.
+ ///
+ /// The same instance so that additional configuration calls can be chained.
+ ///
+ public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType, [NotNull] Action buildAction)
+ {
+ Check.NotEmpty(name, nameof(name));
+ Check.NotNull(clrType, nameof(clrType));
+ Check.NotNull(buildAction, nameof(buildAction));
+
+ buildAction(SharedEntity(name, clrType));
+
+ return this;
+ }
+
///
/// Excludes the given entity type from the model. This method is typically used to remove types from
/// the model that were added by convention.
///
- /// The entity type to be removed from the model.
+ /// The entity type to be removed from the model.
///
/// The same instance so that additional configuration calls can be chained.
///
@@ -234,7 +352,7 @@ public virtual ModelBuilder Ignore()
=> Ignore(typeof(TEntity));
///
- /// Excludes the given entity type from the model. This method is typically used to remove types from
+ /// Excludes an entity type with given CLR type from the model. This method is typically used to remove types from
/// the model that were added by convention.
///
/// The entity type to be removed from the model.
@@ -250,6 +368,23 @@ public virtual ModelBuilder Ignore([NotNull] Type type)
return this;
}
+ ///
+ /// Excludes an entity type with the given name from the model. This method is typically used to remove types from
+ /// the model that were added by convention.
+ ///
+ /// The name of the entity type to be removed from the model.
+ ///
+ /// The same instance so that additional configuration calls can be chained.
+ ///
+ public virtual ModelBuilder Ignore([NotNull] string name)
+ {
+ Check.NotEmpty(name, nameof(name));
+
+ Builder.Ignore(name, ConfigurationSource.Explicit);
+
+ return this;
+ }
+
///
/// Applies configuration that is defined in an instance.
///
@@ -342,6 +477,43 @@ public virtual OwnedEntityTypeBuilder Owned([NotNull] Type type)
return new OwnedEntityTypeBuilder();
}
+ ///
+ ///
+ /// Marks an entity type as shared type. All references to this type will be configured as separate entity types.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// The entity type to be configured.
+ public virtual SharedEntityTypeBuilder SharedEntity()
+ where T : class
+ {
+ Builder.SharedEntity(typeof(T), ConfigurationSource.Explicit);
+
+ return new SharedEntityTypeBuilder();
+ }
+
+ ///
+ ///
+ /// Marks an entity type as shared type. All references to this type will be configured as separate entity types.
+ ///
+ ///
+ /// Shared type entity type is an entity type which can share CLR type with other types in the model but has
+ /// a unique name and always identified by the name.
+ ///
+ ///
+ /// The entity type to be configured.
+ public virtual SharedEntityTypeBuilder SharedEntity([NotNull] Type type)
+ {
+ Check.NotNull(type, nameof(type));
+
+ Builder.SharedEntity(type, ConfigurationSource.Explicit);
+
+ return new SharedEntityTypeBuilder();
+ }
+
///
/// Configures the default to be used for this model.
/// This strategy indicates how the context detects changes to properties for an instance of an entity type.
diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index 2bc2f7989ab..080e322abde 100644
--- a/src/EFCore/Properties/CoreStrings.Designer.cs
+++ b/src/EFCore/Properties/CoreStrings.Designer.cs
@@ -2725,12 +2725,20 @@ public static string InvalidSetSharedType([CanBeNull] object typeName)
typeName);
///
- /// Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type.
+ /// Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model.
///
- public static string DoNotUseUsingEntityOnSharedClrType([CanBeNull] object clrType)
+ public static string CannotMarkShared([CanBeNull] object type)
=> string.Format(
- GetString("DoNotUseUsingEntityOnSharedClrType", nameof(clrType)),
- clrType);
+ GetString("CannotMarkShared", nameof(type)),
+ type);
+
+ ///
+ /// Type '{type}' is not been configured as shared type in the model. Before calling 'UsingEntity', please mark the type as shared or add the entity type in the model as shared entity.
+ ///
+ public static string TypeNotMarkedAsShared([CanBeNull] object type)
+ => string.Format(
+ GetString("TypeNotMarkedAsShared", nameof(type)),
+ type);
private static string GetString(string name, params string[] formatterNames)
{
diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx
index 3b457e18b7c..b6f8e401e7c 100644
--- a/src/EFCore/Properties/CoreStrings.resx
+++ b/src/EFCore/Properties/CoreStrings.resx
@@ -1435,7 +1435,10 @@
Cannot create a DbSet for '{typeName}' because it is configured as an shared type entity type and should be accessed through entity type name based Set method.
-
- Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type.
+
+ Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model.
+
+
+ Type '{type}' is not been configured as shared type in the model. Before calling 'UsingEntity', please mark the type as shared or add the entity type in the model as shared entity.
\ No newline at end of file
diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
index 8cb7aa1c5e8..f05d68fe555 100644
--- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
+++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs
@@ -709,7 +709,15 @@ private NavigationExpansionExpression ProcessDefaultIfEmpty(NavigationExpansionE
QueryableMethods.DefaultIfEmptyWithoutArgument.MakeGenericMethod(source.SourceElementType),
source.Source));
- _entityReferenceOptionalMarkingExpressionVisitor.Visit(source.PendingSelector);
+ var pendingSelector = source.PendingSelector;
+ _entityReferenceOptionalMarkingExpressionVisitor.Visit(pendingSelector);
+ if (!pendingSelector.Type.IsNullableType())
+ {
+ pendingSelector = Expression.Coalesce(
+ Expression.Convert(pendingSelector, pendingSelector.Type.MakeNullable()), pendingSelector.Type.GetDefaultValueConstant());
+ }
+
+ source.ApplySelector(pendingSelector);
return source;
}
diff --git a/src/Shared/ExpressionExtensions.cs b/src/Shared/ExpressionExtensions.cs
index 27b6e2295c7..842ae62e5fd 100644
--- a/src/Shared/ExpressionExtensions.cs
+++ b/src/Shared/ExpressionExtensions.cs
@@ -10,12 +10,6 @@ namespace System.Linq.Expressions
[DebuggerStepThrough]
internal static class ExpressionExtensions
{
- ///
- /// 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.
- ///
public static bool IsNullConstantExpression([NotNull] this Expression expression)
=> RemoveConvert(expression) is ConstantExpression constantExpression
&& constantExpression.Value == null;
diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs
index a01d0a8ae73..533ff2afad9 100644
--- a/src/Shared/SharedTypeExtensions.cs
+++ b/src/Shared/SharedTypeExtensions.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
@@ -466,12 +467,6 @@ private static void ProcessGenericType(StringBuilder builder, Type type, Type[]
builder.Append('>');
}
- ///
- /// 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.
- ///
public static IEnumerable GetNamespaces([NotNull] this Type type)
{
if (_builtInTypeNames.ContainsKey(type))
@@ -492,5 +487,14 @@ public static IEnumerable GetNamespaces([NotNull] this Type type)
}
}
}
+
+ public static ConstantExpression GetDefaultValueConstant(this Type type)
+ => (ConstantExpression)_generateDefaultValueConstantMethod
+ .MakeGenericMethod(type).Invoke(null, Array.Empty