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()); - } - - private static readonly MethodInfo _generateDefaultValueConstantMethod = - typeof(GetValueOrDefaultTranslator).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); - - private static SqlConstantExpression GenerateDefaultValueConstant() - => new SqlConstantExpression(Expression.Constant(default(TDefault)), null); } } diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 789eab9c1d8..7659ac1847e 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -24,6 +24,10 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// 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()); + + private static readonly MethodInfo _generateDefaultValueConstantMethod = + typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); + + private static ConstantExpression GenerateDefaultValueConstant() => Expression.Constant(default(TDefault)); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs index f685f4ac7de..7a1371feb4b 100644 --- a/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/BuiltInDataTypesCosmosTest.cs @@ -80,6 +80,12 @@ FROM root c WHERE ((c[""Discriminator""] = ""BuiltInDataTypes"") AND (c[""Id""] = 13))"); } + [ConditionalFact(Skip = "Issue#21678")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs index cb0b78b1f46..70ca847e7db 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CustomConvertersCosmosTest.cs @@ -146,6 +146,18 @@ FROM root c WHERE (c[""Discriminator""] IN (""Blog"", ""RssBlog"") AND NOT((c[""IndexerVisible""] = ""Aye"")))"); } + [ConditionalFact(Skip = "Issue#27678")] + public override void Optional_owned_with_converter_reading_non_nullable_column() + { + base.Optional_owned_with_converter_reading_non_nullable_column(); + } + + [ConditionalFact(Skip = "Issue#21678")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 57fdd89e9a0..12c4ae26e4f 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4151,6 +4151,18 @@ public override Task Select_distinct_Select_with_client_bindings(bool async) return base.Select_distinct_Select_with_client_bindings(async); } + [ConditionalTheory(Skip = "Issue#21678")] + public override Task Non_nullable_property_through_optional_navigation(bool async) + { + return base.Non_nullable_property_through_optional_navigation(async); + } + + [ConditionalTheory(Skip = "Issue#21678")] + public override Task Max_on_empty_sequence_throws(bool async) + { + return base.Max_on_empty_sequence_throws(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs index 73f076245cf..77d93f0df61 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs @@ -693,9 +693,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { b.OwnsOne( e => e.Throned, b => b.HasData( - new { BartonId = 1, Property = "Property" })); + new { BartonId = 1, Property = "Property", Value = 42 })); b.HasData( - new Barton { Id = 1, Simple = "Simple" }); + new Barton { Id = 1, Simple = "Simple" }, + new Barton { Id = 2, Simple = "Not" }); }); modelBuilder.Entity().HasData( diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index bc7c55ce24a..9595aef8b4c 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -1238,6 +1238,138 @@ public virtual void Many_to_many_join_table_stored_in_snapshot() }); } + + [ConditionalFact] + public virtual void Can_override_table_name_for_many_to_many_join_table_stored_in_snapshot() + { + Test( + builder => + { + var manyToMany = builder + .Entity() + .HasMany(l => l.Rights) + .WithMany(r => r.Lefts) + .UsingEntity(a => a.ToTable("MyJoinTable")); + }, + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""ManyToManyLeftManyToManyRight"", b => + { + b.Property(""ManyToManyLeft_Id"") + .HasColumnType(""int""); + + b.Property(""ManyToManyRight_Id"") + .HasColumnType(""int""); + + b.HasKey(""ManyToManyLeft_Id"", ""ManyToManyRight_Id""); + + b.HasIndex(""ManyToManyRight_Id""); + + b.ToTable(""MyJoinTable""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""Name"") + .HasColumnType(""nvarchar(max)""); + + b.HasKey(""Id""); + + b.ToTable(""ManyToManyLeft""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""Description"") + .HasColumnType(""nvarchar(max)""); + + b.HasKey(""Id""); + + b.ToTable(""ManyToManyRight""); + }); + + modelBuilder.Entity(""ManyToManyLeftManyToManyRight"", b => + { + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft"", null) + .WithMany() + .HasForeignKey(""ManyToManyLeft_Id"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight"", null) + .WithMany() + .HasForeignKey(""ManyToManyRight_Id"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + });", usingSystem: true), + model => + { + var associationEntity = model.FindEntityType("ManyToManyLeftManyToManyRight"); + Assert.NotNull(associationEntity); + Assert.Equal("MyJoinTable", associationEntity.GetTableName()); + Assert.Collection(associationEntity.GetDeclaredProperties(), + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + Assert.True(p.IsShadowProperty()); + }, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + Assert.True(p.IsShadowProperty()); + }); + Assert.Collection(associationEntity.FindDeclaredPrimaryKey().Properties, + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + }, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + }); + Assert.Collection(associationEntity.GetDeclaredForeignKeys(), + fk => + { + Assert.Equal("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyLeft", fk.PrincipalEntityType.Name); + Assert.Collection(fk.PrincipalKey.Properties, + p => + { + Assert.Equal("Id", p.Name); + }); + Assert.Collection(fk.Properties, + p => + { + Assert.Equal("ManyToManyLeft_Id", p.Name); + }); + }, + fk => + { + Assert.Equal("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+ManyToManyRight", fk.PrincipalEntityType.Name); + Assert.Collection(fk.PrincipalKey.Properties, + p => + { + Assert.Equal("Id", p.Name); + }); + Assert.Collection(fk.Properties, + p => + { + Assert.Equal("ManyToManyRight_Id", p.Name); + }); + }); + }); + } + [ConditionalFact] public virtual void TableName_preserved_when_generic() { diff --git a/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs index 4f3405c8fb0..20968ab30e3 100644 --- a/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/BuiltInDataTypesInMemoryTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore @@ -14,6 +15,12 @@ public BuiltInDataTypesInMemoryTest(BuiltInDataTypesInMemoryFixture fixture) { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + public class BuiltInDataTypesInMemoryFixture : BuiltInDataTypesFixtureBase { protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; diff --git a/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs index dced737313b..549be923c08 100644 --- a/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/ConvertToProviderTypesInMemoryTest.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; namespace Microsoft.EntityFrameworkCore { @@ -14,6 +15,12 @@ public ConvertToProviderTypesInMemoryTest(ConvertToProviderTypesInMemoryFixture { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + public class ConvertToProviderTypesInMemoryFixture : ConvertToProviderTypesFixtureBase { protected override ITestStoreFactory TestStoreFactory => InMemoryTestStoreFactory.Instance; diff --git a/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs index e3768b48814..ed278a74517 100644 --- a/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/CustomConvertersInMemoryTest.cs @@ -14,6 +14,12 @@ public CustomConvertersInMemoryTest(CustomConvertersInMemoryFixture fixture) { } + [ConditionalFact(Skip = "Issue#21680")] + public override void Optional_datetime_reading_null_from_database() + { + base.Optional_datetime_reading_null_from_database(); + } + // Disabled: In-memory database is case-sensitive public override void Can_insert_and_read_back_with_case_insensitive_string_key() { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs index eca74b677fe..06ee1121790 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs @@ -44,5 +44,11 @@ public override Task Select_subquery_single_nested_subquery2(bool async) { return base.Select_subquery_single_nested_subquery2(async); } + + [ConditionalTheory(Skip = "issue #17539")] + public override Task Union_over_entities_with_different_nullability(bool async) + { + return base.Union_over_entities_with_different_nullability(async); + } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs index 5bf97e7336f..6c99a4389db 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsWeakQueryInMemoryTest.cs @@ -42,6 +42,12 @@ public override Task Where_nav_prop_reference_optional1_via_DefaultIfEmpty(bool return base.Where_nav_prop_reference_optional1_via_DefaultIfEmpty(async); } + [ConditionalTheory(Skip = "Issue#17539")] + public override Task Where_nav_prop_reference_optional2_via_DefaultIfEmpty(bool async) + { + return base.Where_nav_prop_reference_optional2_via_DefaultIfEmpty(async); + } + [ConditionalTheory(Skip = "Issue#17539")] public override Task Optional_navigation_propagates_nullability_to_manually_created_left_join2(bool async) { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs index 6ffb0a6463e..f9ef1b06484 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -24,17 +25,32 @@ public NorthwindAggregateOperatorsQueryInMemoryTest( // InMemory can throw server side exception public override void Average_no_data_subquery() { - Assert.Throws(() => base.Average_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList()).Message); } public override void Max_no_data_subquery() { - Assert.Throws(() => base.Max_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList()).Message); } public override void Min_no_data_subquery() { - Assert.Throws(() => base.Min_no_data_subquery()); + using var context = CreateContext(); + + Assert.Equal( + "Sequence contains no elements", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList()).Message); } public override Task Collection_Last_member_access_in_projection_translated(bool async) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs index ced0ba9b9a1..fa2cc302a5c 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; using Xunit.Abstractions; @@ -105,5 +107,17 @@ public override Task Using_string_Equals_with_StringComparison_throws_informativ [ConditionalTheory(Skip = "issue #17386")] public override Task Using_static_string_Equals_with_StringComparison_throws_informative_error(bool async) => base.Using_static_string_Equals_with_StringComparison_throws_informative_error(async); + + public override async Task Max_on_empty_sequence_throws(bool async) + { + using var context = CreateContext(); + var query = context.Set().Select(e => new { Max = e.Orders.Max(o => o.OrderID) }); + + var message = async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message; + + Assert.Equal("Sequence contains no elements", message); + } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index ceb992edfb0..f668c4edd77 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -885,8 +885,8 @@ public virtual Task Select_IndexOf(bool async) { return AssertQueryScalar( async, - ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.IndexOf("oo")), - ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.MaybeScalar(x => x.IndexOf("oo")) ?? 0), + ss => ss.Set().OrderBy(e => e.Id).Select(e => (int?)e.NullableStringA.IndexOf("oo")), + ss => ss.Set().OrderBy(e => e.Id).Select(e => e.NullableStringA.MaybeScalar(x => x.IndexOf("oo"))), assertOrder: true); } diff --git a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs index ed6b31c8271..f7d284e2427 100644 --- a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs +++ b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs @@ -2078,6 +2078,22 @@ public virtual void Object_to_string_conversion() Assert.Equal(expected.Char, actual.Char); } + [ConditionalFact] + public virtual void Optional_datetime_reading_null_from_database() + { + using var context = CreateContext(); + var expected = context.Set().ToList() + .Select(e => new { DT = e.DateTimeOffset == null ? (DateTime?)null : e.DateTimeOffset.Value.DateTime.Date }).ToList(); + + var actual = context.Set() + .Select(e => new { DT = e.DateTimeOffset == null ? (DateTime?)null : e.DateTimeOffset.Value.DateTime.Date }).ToList(); + + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].DT, actual[i].DT); + } + } + public abstract class BuiltInDataTypesFixtureBase : SharedStoreFixtureBase { protected override string StoreName { get; } = "BuiltInDataTypes"; @@ -2334,6 +2350,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con AnimalId = 1, Method = IdentificationMethod.EarTag }); + + modelBuilder.Entity() + .HasData( + new DateTimeEnclosure + { + Id = 1, + DateTimeOffset = new DateTimeOffset(2020, 3, 12, 1, 1, 1, new TimeSpan(3, 0, 0)) + }, + new DateTimeEnclosure + { + Id = 2 + }); } protected static void MakeRequired(ModelBuilder modelBuilder) @@ -3133,5 +3161,11 @@ protected enum IdentificationMethod EarTag, Rfid } + + protected class DateTimeEnclosure + { + public int Id { get; set; } + public DateTimeOffset? DateTimeOffset { get; set; } + } } } diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index c14a52e05a8..a6281c93e1d 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -667,6 +667,26 @@ protected enum Roles public override void Object_to_string_conversion() {} + [ConditionalFact] + public virtual void Optional_owned_with_converter_reading_non_nullable_column() + { + using var context = CreateContext(); + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Set().Select(e => new { e.OwnedWithConverter.Value }).ToList()).Message); + } + + protected class Parent + { + public int Id { get; set; } + public OwnedWithConverter OwnedWithConverter { get; set; } + } + + protected class OwnedWithConverter + { + public int Value { get; set; } + } public abstract class CustomConvertersFixtureBase : BuiltInDataTypesFixtureBase { protected override string StoreName { get; } = "CustomConverters"; @@ -1129,6 +1149,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con Roles = new List { Roles.Seller } }); }); + + modelBuilder.Entity( + b => + { + b.OwnsOne( + e => e.OwnedWithConverter, + ob => + { + ob.Property(i => i.Value).HasConversion(); + ob.HasData(new { ParentId = 1, Value = 42 }); + }); + + b.HasData( + new Parent { Id = 1 }, + new Parent { Id = 2 }); + }); } private static class StringToDictionarySerializer diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 5263946bc5b..deb16e8e76f 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -4709,14 +4709,14 @@ public virtual Task Union_over_entities_with_different_nullability(bool async) .Concat(ss.Set().GroupJoin(ss.Set(), l2 => l2.Level1_Optional_Id, l1 => l1.Id, (l2, l1s) => new { l2, l1s }) .SelectMany(g => g.l1s.DefaultIfEmpty(), (g, l1) => new { l1, g.l2 }) .Where(e => e.l1.Equals(null))) - .Select(e => e.l1.Id), + .Select(e => (int?)e.l1.Id), ss => ss.Set() .GroupJoin(ss.Set(), l1 => l1.Id, l2 => l2.Level1_Optional_Id, (l1, l2s) => new { l1, l2s }) .SelectMany(g => g.l2s.DefaultIfEmpty(), (g, l2) => new { g.l1, l2 }) .Concat(ss.Set().GroupJoin(ss.Set(), l2 => l2.Level1_Optional_Id, l1 => l1.Id, (l2, l1s) => new { l2, l1s }) .SelectMany(g => g.l1s.DefaultIfEmpty(), (g, l1) => new { l1, g.l2 }) .Where(e => e.l1 == null)) - .Select(e => e.l1.MaybeScalar(x => x.Id) ?? 0)); + .Select(e => e.l1.MaybeScalar(x => x.Id))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index d80bb52c69b..18a1b68ac71 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -5430,26 +5430,6 @@ public virtual Task Select_subquery_boolean_empty_with_pushdown(bool async) ss => ss.Set().Select(g => (bool?)null)); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - return AssertQueryScalar( - async, - ss => ss.Set().Select(g => g.Weapons.Where(w => w.Name == "BFG").OrderBy(w => w.Id).FirstOrDefault().IsAutomatic), - ss => ss.Set().Select(g => false)); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - return AssertQueryScalar( - async, - ss => ss.Set().Select(g => g.Weapons.Where(w => w.Name == "BFG").OrderBy(w => w.Id).FirstOrDefault().Id), - ss => ss.Set().Select(g => 0)); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_subquery_distinct_singleordefault_boolean1(bool async) @@ -6801,13 +6781,13 @@ public virtual Task GroupBy_with_boolean_groupin_key_thru_navigation_access(bool return AssertQuery( async, ss => ss.Set() - .GroupBy(t => new { t.Gear.HasSoulPatch, t.Gear.Squad.Name }) + .GroupBy(t => new { HasSoulPatch = (bool?)t.Gear.HasSoulPatch, t.Gear.Squad.Name }) .Select(g => new { g.Key.HasSoulPatch, Name = g.Key.Name.ToLower() }), ss => ss.Set() .GroupBy( t => new { - HasSoulPatch = t.Gear.MaybeScalar(x => x.HasSoulPatch) ?? false, + HasSoulPatch = t.Gear.MaybeScalar(x => x.HasSoulPatch), Name = t.Gear.Squad.Name }) .Select(g => new { g.Key.HasSoulPatch, Name = g.Key.Name.Maybe(x => x.ToLower()) }), diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs index 4526697c810..cad533b3712 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs @@ -392,8 +392,11 @@ public virtual void Min_no_data_cast_to_nullable() public virtual void Min_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Min(o => o.OrderID)).ToList()).Message); } [ConditionalFact] @@ -421,8 +424,11 @@ public virtual void Max_no_data_cast_to_nullable() public virtual void Max_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Max(o => o.OrderID)).ToList()).Message); } [ConditionalFact] @@ -450,8 +456,11 @@ public virtual void Average_no_data_cast_to_nullable() public virtual void Average_no_data_subquery() { using var context = CreateContext(); - // Verify that it does not throw - context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList(); + + Assert.Equal( + "Nullable object must have a value.", + Assert.Throws( + () => context.Customers.Select(c => c.Orders.Where(o => o.OrderID == -1).Average(o => o.OrderID)).ToList()).Message); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 822f19f9710..c5b2ac18efe 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -15,6 +15,7 @@ using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.Utilities; using Xunit; +using Xunit.Sdk; #pragma warning disable RCS1202 // Avoid NullReferenceException. @@ -6021,7 +6022,7 @@ public virtual Task ToList_over_string(bool async) async, ss => ss.Set().OrderBy(c => c.CustomerID).Select(e => new { Property = e.City.ToList() }), assertOrder: true, - elementAsserter: (e,a) => Assert.True(e.Property.SequenceEqual(a.Property))); + elementAsserter: (e, a) => Assert.True(e.Property.SequenceEqual(a.Property))); } [ConditionalTheory] @@ -6047,5 +6048,31 @@ public virtual Task AsEnumerable_over_string(bool async) } private static int ClientMethod(int s) => s; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Non_nullable_property_through_optional_navigation(bool async) + { + Assert.Equal( + "Nullable object must have a value.", + (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Region.Length })))).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Max_on_empty_sequence_throws(bool async) + { + using var context = CreateContext(); + var query = context.Set().Select(e => new { Max = e.Orders.Max(o => o.OrderID) }); + + var message = async + ? (await Assert.ThrowsAsync(() => query.ToListAsync())).Message + : Assert.Throws(() => query.ToList()).Message; + + Assert.Equal("Nullable object must have a value.", message); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs index 41f7ee23f2b..a957bb1ce26 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs @@ -946,10 +946,10 @@ public virtual Task Project_single_element_from_collection_with_OrderBy_Distinct async, ss => ss.Set() .Select(c => c.Orders.OrderBy(o => o.OrderID).Select(o => o.CustomerID).Distinct().FirstOrDefault()) - .Select(e => e.Length), + .Select(e => (int?)e.Length), ss => ss.Set() .Select(c => c.Orders.OrderBy(o => o.OrderID).Select(o => o.CustomerID).Distinct().FirstOrDefault()) - .Select(e => e == null ? 0 : e.Length)); + .Select(e => e.MaybeScalar(e => e.Length))); } [ConditionalTheory] @@ -1449,8 +1449,9 @@ public virtual Task Project_non_nullable_value_after_FirstOrDefault_on_empty_col return AssertQueryScalar( async, ss => ss.Set().Select( - c => ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().Length), - ss => ss.Set().Select(c => 0)); + c => (int?)ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().Length), + ss => ss.Set().Select( + c => (int?)ss.Set().Where(o => o.CustomerID == "John Doe").Select(o => o.CustomerID).FirstOrDefault().MaybeScalar(e => e.Length))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index 632133a1abc..33d21bba0df 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -810,6 +810,19 @@ public virtual Task GroupBy_with_multiple_aggregates_on_owned_navigation_propert })); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + + public virtual async Task Non_nullable_property_through_optional_navigation(bool async) + { + Assert.Equal( + "Nullable object must have a value.", + (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Throned.Value })))).Message); + } + protected virtual DbContext CreateContext() => Fixture.CreateContext(); public abstract class OwnedQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase @@ -1078,6 +1091,7 @@ public IReadOnlyDictionary GetEntityAsserters() Assert.Equal(e == null, a == null); if (a != null) { + Assert.Equal(((Throned)e).Value, ((Throned)a).Value); Assert.Equal(((Throned)e).Property, ((Throned)a).Property); } } @@ -1291,9 +1305,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { b.OwnsOne( e => e.Throned, b => b.HasData( - new { BartonId = 1, Property = "Property" })); + new { BartonId = 1, Property = "Property", Value = 42 })); b.HasData( - new Barton { Id = 1, Simple = "Simple" }); + new Barton { Id = 1, Simple = "Simple" }, + new Barton { Id = 2, Simple = "Not" }); }); modelBuilder.Entity().HasData( @@ -1509,7 +1524,12 @@ private static IReadOnlyList CreateBartons() { Id = 1, Simple = "Simple", - Throned = new Throned { Property = "Property" } + Throned = new Throned { Property = "Property", Value = 42 } + }, + new Barton + { + Id = 2, + Simple = "Not", } }; @@ -1744,6 +1764,7 @@ protected class Fink protected class Throned { + public int Value { get; set; } public string Property { get; set; } } } diff --git a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs index 5749dabd135..bc8b31e7cfd 100644 --- a/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SpatialQueryTestBase.cs @@ -340,19 +340,6 @@ public virtual Task Disjoint_with_cast_to_nullable(bool async) elementSorter: x => x.Id); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Disjoint_without_cast_to_nullable(bool async) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(1, 1)); - - return AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Disjoint = e.Polygon.Disjoint(point) }), - ss => ss.Set().Select(e => new { e.Id, Disjoint = (e.Polygon == null ? false : e.Polygon.Disjoint(point)) }), - elementSorter: x => x.Id); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Disjoint_with_null_check(bool async) @@ -365,28 +352,6 @@ public virtual Task Disjoint_with_null_check(bool async) elementSorter: x => x.Id); } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual Task Distance_without_null_check(bool async) - { - var point = Fixture.GeometryFactory.CreatePoint(new Coordinate(0, 1)); - - return AssertQuery( - async, - ss => ss.Set().Select(e => new { e.Id, Distance = e.Point.Distance(point) }), - ss => ss.Set().Select(e => new { e.Id, Distance = e.Point == null ? default : e.Point.Distance(point) }), - elementSorter: e => e.Id, - elementAsserter: (e, a) => - { - Assert.Equal(e.Id, a.Id); - - if (AssertDistances) - { - Assert.Equal(e.Distance, a.Distance); - } - }); - } - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Distance_with_null_check(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs index 4ec43cb9be2..f6a70026ec2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs @@ -2593,6 +2593,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable bigint] [Precision = 19 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] MappedDataTypes.BoolAsBit ---> [bit] diff --git a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs index 78d856b2df1..3908752dfc5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs @@ -143,6 +143,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable decimal] [Precision = 20 Scale = 0] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] MaxLengthDataTypes.ByteArray5 ---> [nullable nvarchar] [MaxLength = 8] diff --git a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs index 5dfc070f089..e9c8c18cde2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs @@ -154,6 +154,8 @@ public virtual void Columns_have_expected_data_types() CollectionEnum.Roles ---> [nullable nvarchar] [MaxLength = -1] CollectionScalar.Id ---> [int] [Precision = 10 Scale = 0] CollectionScalar.Tags ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable datetimeoffset] [Precision = 7] +DateTimeEnclosure.Id ---> [int] [Precision = 10 Scale = 0] EmailTemplate.Id ---> [uniqueidentifier] EmailTemplate.TemplateType ---> [int] [Precision = 10 Scale = 0] EntityWithValueWrapper.Id ---> [int] [Precision = 10 Scale = 0] @@ -169,6 +171,8 @@ public virtual void Columns_have_expected_data_types() NonNullableDependent.PrincipalId ---> [int] [Precision = 10 Scale = 0] NullablePrincipal.Id ---> [int] [Precision = 10 Scale = 0] Order.Id ---> [nvarchar] [MaxLength = 450] +Parent.Id ---> [int] [Precision = 10 Scale = 0] +Parent.OwnedWithConverter_Value ---> [nullable nvarchar] [MaxLength = 64] Person.Id ---> [int] [Precision = 10 Scale = 0] Person.Name ---> [nullable nvarchar] [MaxLength = -1] Person.SSN ---> [nullable int] [Precision = 10 Scale = 0] diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs index 8a68e987538..b7017df1f37 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs @@ -147,6 +147,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable varbinary] [MaxLength = 4] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable varbinary] [MaxLength = 8] BuiltInNullableDataTypesShadow.TestString ---> [nullable varbinary] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable varbinary] [MaxLength = 12] +DateTimeEnclosure.Id ---> [varbinary] [MaxLength = 4] EmailTemplate.Id ---> [varbinary] [MaxLength = 16] EmailTemplate.TemplateType ---> [varbinary] [MaxLength = 4] MaxLengthDataTypes.ByteArray5 ---> [nullable varbinary] [MaxLength = 5] diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs index 487bfe6bca6..e3ad946a16b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs @@ -148,6 +148,8 @@ public virtual void Columns_have_expected_data_types() BuiltInNullableDataTypesShadow.TestNullableUnsignedInt32 ---> [nullable nvarchar] [MaxLength = 64] BuiltInNullableDataTypesShadow.TestNullableUnsignedInt64 ---> [nullable nvarchar] [MaxLength = 64] BuiltInNullableDataTypesShadow.TestString ---> [nullable nvarchar] [MaxLength = -1] +DateTimeEnclosure.DateTimeOffset ---> [nullable nvarchar] [MaxLength = 48] +DateTimeEnclosure.Id ---> [nvarchar] [MaxLength = 64] EmailTemplate.Id ---> [nvarchar] [MaxLength = 36] EmailTemplate.TemplateType ---> [nvarchar] [MaxLength = -1] MaxLengthDataTypes.ByteArray5 ---> [nullable nvarchar] [MaxLength = 8] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs b/test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs similarity index 98% rename from test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs rename to test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs index b76b7956113..23c028ca4fc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NavigationTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/NavigationTest.cs @@ -9,8 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore.Query +namespace Microsoft.EntityFrameworkCore { public class NavigationTest : IClassFixture { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index cb3361de315..0801f4292a4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -2176,10 +2176,10 @@ public override async Task Select_join_with_key_selector_being_a_subquery(bool a AssertSql( @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] FROM [LevelOne] AS [l] -INNER JOIN [LevelTwo] AS [l0] ON [l].[Id] = ( +INNER JOIN [LevelTwo] AS [l0] ON [l].[Id] = COALESCE(( SELECT TOP(1) [l1].[Id] FROM [LevelTwo] AS [l1] - ORDER BY [l1].[Id])"); + ORDER BY [l1].[Id]), 0)"); } public override async Task Contains_with_subquery_optional_navigation_and_constant_item(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index e2217dd8b97..432db54bd79 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -529,29 +529,23 @@ public override async Task Where_enum_has_flag_subquery(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE (([g].[Rank] & ( +WHERE ([g].[Rank] & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL", + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)", // @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ((2 & ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( +WHERE (2 & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL"); + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)"); } public override async Task Where_enum_has_flag_subquery_with_pushdown(bool async) @@ -1263,11 +1257,11 @@ public override async Task Where_subquery_boolean(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ( +WHERE COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) = CAST(1 AS bit)"); + ORDER BY [w].[Id]), CAST(0 AS bit)) = CAST(1 AS bit)"); } public override async Task Where_subquery_boolean_with_pushdown(bool async) @@ -1291,14 +1285,14 @@ public override async Task Where_subquery_distinct_firstordefault_boolean(bool a AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_firstordefault_boolean_with_pushdown(bool async) @@ -1343,13 +1337,13 @@ public override async Task Where_subquery_distinct_singleordefault_boolean1(bool AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) = CAST(1 AS bit)) + ) AS [t]), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1360,10 +1354,10 @@ public override async Task Where_subquery_distinct_singleordefault_boolean2(bool AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) = CAST(1 AS bit)) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1427,14 +1421,14 @@ public override async Task Where_subquery_distinct_orderby_firstordefault_boolea AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_orderby_firstordefault_boolean_with_pushdown(bool async) @@ -4704,10 +4698,10 @@ public override async Task Project_one_value_type_from_empty_collection(bool asy await base.Project_one_value_type_from_empty_collection(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) [g].[SquadId] FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [SquadId] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [SquadId] FROM [Squads] AS [s] WHERE [s].[Name] = N'Kilo'"); } @@ -4763,10 +4757,10 @@ public override async Task Select_subquery_projecting_single_constant_int(bool a await base.Select_subquery_projecting_single_constant_int(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) 42 FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [Gear] FROM [Squads] AS [s]"); } @@ -4787,10 +4781,10 @@ public override async Task Select_subquery_projecting_single_constant_bool(bool await base.Select_subquery_projecting_single_constant_bool(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) CAST(1 AS bit) FROM [Gears] AS [g] - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), CAST(0 AS bit)) AS [Gear] FROM [Squads] AS [s]"); } @@ -4928,11 +4922,11 @@ public override async Task Include_collection_with_complex_OrderBy3(bool async) FROM [Gears] AS [g] LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname]) AND ([g].[SquadId] = [g0].[LeaderSquadId]) WHERE [g].[Discriminator] = N'Officer' -ORDER BY ( +ORDER BY COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]"); + ORDER BY [w].[Id]), CAST(0 AS bit)), [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]"); } public override async Task Correlated_collection_with_complex_OrderBy(bool async) @@ -4970,10 +4964,10 @@ FROM [Gears] AS [g0] ORDER BY ( SELECT COUNT(*) FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = ( + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = COALESCE(( SELECT TOP(1) [g1].[HasSoulPatch] FROM [Gears] AS [g1] - WHERE [g1].[Nickname] = N'Marcus'))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + WHERE [g1].[Nickname] = N'Marcus'), CAST(0 AS bit)))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Cast_to_derived_type_after_OfType_works(bool async) @@ -4991,11 +4985,11 @@ public override async Task Select_subquery_boolean(bool async) await base.Select_subquery_boolean(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g]"); } @@ -5030,11 +5024,11 @@ public override async Task Select_subquery_int_with_outside_cast_and_coalesce(bo await base.Select_subquery_int_with_outside_cast_and_coalesce(async); AssertSql( - @"SELECT COALESCE(( + @"SELECT COALESCE(COALESCE(( SELECT TOP(1) [w].[Id] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), 42) + ORDER BY [w].[Id]), 0), 42) FROM [Gears] AS [g]"); } @@ -5073,11 +5067,11 @@ public override async Task Select_subquery_boolean_empty(bool async) await base.Select_subquery_boolean_empty(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g]"); } @@ -5094,44 +5088,18 @@ FROM [Weapons] AS [w] FROM [Gears] AS [g]"); } - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[IsAutomatic] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g]"); - } - - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[Id] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g]"); - } - public override async Task Select_subquery_distinct_singleordefault_boolean1(bool async) { await base.Select_subquery_distinct_singleordefault_boolean1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5141,10 +5109,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean2(boo await base.Select_subquery_distinct_singleordefault_boolean2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5170,13 +5138,13 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } @@ -5186,10 +5154,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')), CAST(0 AS bit)) FROM [Gears] AS [g] WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index ebc9f621a0d..a0031f14a35 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -441,7 +441,7 @@ public override async Task Default_if_empty_top_level_projection(bool async) await base.Default_if_empty_top_level_projection(async); AssertSql( - @"SELECT [t].[EmployeeID] + @"SELECT COALESCE([t].[EmployeeID], 0) FROM ( SELECT NULL AS [empty] ) AS [empty] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs index 03fbc7054d3..5bcb32ff181 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindNavigationsQuerySqlServerTest.cs @@ -896,11 +896,11 @@ public override async Task Project_single_scalar_value_subquery_in_query_with_op AssertSql( @"@__p_0='3' -SELECT [t].[OrderID], ( +SELECT [t].[OrderID], COALESCE(( SELECT TOP(1) [o].[OrderID] FROM [Order Details] AS [o] WHERE [t].[OrderID] = [o].[OrderID] - ORDER BY [o].[OrderID], [o].[ProductID]) AS [OrderDetail], [c].[City] + ORDER BY [o].[OrderID], [o].[ProductID]), 0) AS [OrderDetail], [c].[City] FROM ( SELECT TOP(@__p_0) [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] FROM [Orders] AS [o0] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 52c397f74db..24525477e43 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -371,13 +371,13 @@ public override void Select_nested_collection_multi_level4() base.Select_nested_collection_multi_level4(); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) ( SELECT COUNT(*) FROM [Order Details] AS [o] WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] > 10)) FROM [Orders] AS [o0] - WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -387,8 +387,8 @@ public override void Select_nested_collection_multi_level5() base.Select_nested_collection_multi_level5(); AssertSql( - @"SELECT ( - SELECT TOP(1) ( + @"SELECT COALESCE(( + SELECT TOP(1) COALESCE(( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] WHERE ([o1].[OrderID] = [o].[OrderID]) AND (([o].[OrderID] <> ( @@ -397,9 +397,9 @@ FROM [Orders] AS [o0] WHERE [c].[CustomerID] = [o0].[CustomerID])) OR ( SELECT COUNT(*) FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) IS NULL)) + WHERE [c].[CustomerID] = [o0].[CustomerID]) IS NULL)), 0) FROM [Orders] AS [o1] - WHERE ([c].[CustomerID] = [o1].[CustomerID]) AND ([o1].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o1].[CustomerID]) AND ([o1].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -409,13 +409,13 @@ public override void Select_nested_collection_multi_level6() base.Select_nested_collection_multi_level6(); AssertSql( - @"SELECT ( - SELECT TOP(1) ( + @"SELECT COALESCE(( + SELECT TOP(1) COALESCE(( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] - WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] <> CAST(LEN([c].[CustomerID]) AS int))) + WHERE ([o0].[OrderID] = [o].[OrderID]) AND ([o].[OrderID] <> CAST(LEN([c].[CustomerID]) AS int))), 0) FROM [Orders] AS [o0] - WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)) AS [Order] + WHERE ([c].[CustomerID] = [o0].[CustomerID]) AND ([o0].[OrderID] < 10500)), 0) AS [Order] FROM [Customers] AS [c] WHERE [c].[CustomerID] LIKE N'A%'"); } @@ -774,7 +774,7 @@ public override async Task Project_single_element_from_collection_with_OrderBy_o await base.Project_single_element_from_collection_with_OrderBy_over_navigation_Take_and_FirstOrDefault(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[OrderID] FROM ( SELECT TOP(1) [o].[OrderID], [o].[ProductID], [p].[ProductID] AS [ProductID0], [p].[ProductName] @@ -783,7 +783,7 @@ FROM [Order Details] AS [o] WHERE [o0].[OrderID] = [o].[OrderID] ORDER BY [p].[ProductName] ) AS [t] - ORDER BY [t].[ProductName]) + ORDER BY [t].[ProductName]), 0) FROM [Orders] AS [o0] WHERE [o0].[OrderID] < 10300"); } @@ -1127,11 +1127,11 @@ public override async Task FirstOrDefault_over_empty_collection_of_value_type_re await base.FirstOrDefault_over_empty_collection_of_value_type_returns_correct_results(async); AssertSql( - @"SELECT [c].[CustomerID], ( + @"SELECT [c].[CustomerID], COALESCE(( SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID]) AS [OrderId] + ORDER BY [o].[OrderID]), 0) AS [OrderId] FROM [Customers] AS [c] WHERE [c].[CustomerID] = N'FISSA'"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index b24a79bc4b1..f8c8a4de9cf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -153,14 +153,14 @@ public override async Task Navigation_rewrite_on_owned_collection_with_compositi await base.Navigation_rewrite_on_owned_collection_with_composition(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) CASE WHEN [o].[Id] <> 42 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END FROM [Order] AS [o] WHERE [o0].[Id] = [o].[ClientId] - ORDER BY [o].[Id]) + ORDER BY [o].[Id]), CAST(0 AS bit)) FROM [OwnedPerson] AS [o0] ORDER BY [o0].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index bfab233df86..0770508dbf1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6293,14 +6293,14 @@ public virtual void Let_multiple_references_with_reference_to_outer_2() @"SELECT [a0].[Id], [a0].[ActivityTypeId], [a0].[DateTime], [a0].[Points], ( SELECT TOP(1) [c].[Id] FROM [CompetitionSeasons] AS [c] - WHERE ([c].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c].[EndDate])) AS [CompetitionSeasonId], COALESCE([a0].[Points], ( + WHERE ([c].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c].[EndDate])) AS [CompetitionSeasonId], COALESCE([a0].[Points], COALESCE(( SELECT TOP(1) [a].[Points] FROM [ActivityTypePoints12456] AS [a] INNER JOIN [CompetitionSeasons] AS [c0] ON [a].[CompetitionSeasonId] = [c0].[Id] WHERE ([a1].[Id] = [a].[ActivityTypeId]) AND ([c0].[Id] = ( SELECT TOP(1) [c1].[Id] FROM [CompetitionSeasons] AS [c1] - WHERE ([c1].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c1].[EndDate]))))) AS [Points] + WHERE ([c1].[StartDate] <= [a0].[DateTime]) AND ([a0].[DateTime] < [c1].[EndDate])))), 0)) AS [Points] FROM [Activities] AS [a0] INNER JOIN [ActivityType12456] AS [a1] ON [a0].[ActivityTypeId] = [a1].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs index 75e85d501a7..cf7fc87474f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeographyTest.cs @@ -188,17 +188,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0xE6100000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) -SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] -FROM [PolygonEntity] AS [p]"); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0xE6100000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) - SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] FROM [PolygonEntity] AS [p]"); } @@ -217,17 +206,6 @@ END AS [Disjoint] FROM [PolygonEntity] AS [p]"); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0xE6100000010C000000000000F03F0000000000000000' (Size = 22) (DbType = Object) - -SELECT [p].[Id], [p].[Point].STDistance(@__point_0) AS [Distance] -FROM [PointEntity] AS [p]"); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs index ff96b769104..61716294eab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SpatialQuerySqlServerGeometryTest.cs @@ -275,17 +275,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0x00000000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) -SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] -FROM [PolygonEntity] AS [p]"); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0x00000000010C000000000000F03F000000000000F03F' (Size = 22) (DbType = Object) - SELECT [p].[Id], [p].[Polygon].STDisjoint(@__point_0) AS [Disjoint] FROM [PolygonEntity] AS [p]"); } @@ -304,17 +293,6 @@ END AS [Disjoint] FROM [PolygonEntity] AS [p]"); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0x00000000010C0000000000000000000000000000F03F' (Size = 22) (DbType = Object) - -SELECT [p].[Id], [p].[Point].STDistance(@__point_0) AS [Distance] -FROM [PointEntity] AS [p]"); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index e026b220ee5..93ec44fcd12 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -719,19 +719,15 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE (([g].[Rank] & ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( +WHERE ([g].[Rank] & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL", + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)", // @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN CAST(1 AS bit) @@ -739,19 +735,15 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ((2 & ( +WHERE (2 & COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) = ( + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)) = COALESCE(( SELECT TOP(1) [g0].[Rank] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId])) OR ( - SELECT TOP(1) [g0].[Rank] - FROM [Gears] AS [g0] - LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) - ORDER BY [g0].[Nickname], [g0].[SquadId]) IS NULL"); + ORDER BY [g0].[Nickname], [g0].[SquadId]), 0)"); } public override async Task Where_enum_has_flag_subquery_with_pushdown(bool async) @@ -1670,11 +1662,11 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ( +WHERE COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) = CAST(1 AS bit)"); + ORDER BY [w].[Id]), CAST(0 AS bit)) = CAST(1 AS bit)"); } public override async Task Where_subquery_boolean_with_pushdown(bool async) @@ -1706,14 +1698,14 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_firstordefault_boolean_with_pushdown(bool async) @@ -1770,13 +1762,13 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) = CAST(1 AS bit)) + ) AS [t]), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1791,10 +1783,10 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) = CAST(1 AS bit)) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) = CAST(1 AS bit)) ORDER BY [g].[Nickname]"); } @@ -1874,14 +1866,14 @@ ELSE CAST(0 AS bit) END AS [IsOfficer] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) -WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (( +WHERE ([g].[HasSoulPatch] = CAST(1 AS bit)) AND (COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] ) AS [t] - ORDER BY [t].[Id]) = CAST(1 AS bit))"); + ORDER BY [t].[Id]), CAST(0 AS bit)) = CAST(1 AS bit))"); } public override async Task Where_subquery_distinct_orderby_firstordefault_boolean_with_pushdown(bool async) @@ -6087,11 +6079,11 @@ public override async Task Project_one_value_type_from_empty_collection(bool asy await base.Project_one_value_type_from_empty_collection(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) [g].[SquadId] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [SquadId] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [SquadId] FROM [Squads] AS [s] WHERE [s].[Name] = N'Kilo'"); } @@ -6149,11 +6141,11 @@ public override async Task Select_subquery_projecting_single_constant_int(bool a await base.Select_subquery_projecting_single_constant_int(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) 42 FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), 0) AS [Gear] FROM [Squads] AS [s]"); } @@ -6175,11 +6167,11 @@ public override async Task Select_subquery_projecting_single_constant_bool(bool await base.Select_subquery_projecting_single_constant_bool(async); AssertSql( - @"SELECT [s].[Name], ( + @"SELECT [s].[Name], COALESCE(( SELECT TOP(1) CAST(1 AS bit) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) - WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))) AS [Gear] + WHERE ([s].[Id] = [g].[SquadId]) AND ([g].[HasSoulPatch] = CAST(1 AS bit))), CAST(0 AS bit)) AS [Gear] FROM [Squads] AS [s]"); } @@ -6353,11 +6345,11 @@ FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) ) AS [t] ON ([g].[Nickname] = [t].[LeaderNickname]) AND ([g].[SquadId] = [t].[LeaderSquadId]) WHERE [o].[Nickname] IS NOT NULL -ORDER BY ( +ORDER BY COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + ORDER BY [w].[Id]), CAST(0 AS bit)), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Correlated_collection_with_complex_OrderBy(bool async) @@ -6405,11 +6397,11 @@ WHERE [o].[Nickname] IS NOT NULL ORDER BY ( SELECT COUNT(*) FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = ( + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = COALESCE(( SELECT TOP(1) [g1].[HasSoulPatch] FROM [Gears] AS [g1] LEFT JOIN [Officers] AS [o1] ON ([g1].[Nickname] = [o1].[Nickname]) AND ([g1].[SquadId] = [o1].[SquadId]) - WHERE [g1].[Nickname] = N'Marcus'))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); + WHERE [g1].[Nickname] = N'Marcus'), CAST(0 AS bit)))), [g].[Nickname], [g].[SquadId], [t].[Nickname], [t].[SquadId]"); } public override async Task Cast_to_derived_type_after_OfType_works(bool async) @@ -6428,11 +6420,11 @@ public override async Task Select_subquery_boolean(bool async) await base.Select_subquery_boolean(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6470,11 +6462,11 @@ public override async Task Select_subquery_int_with_outside_cast_and_coalesce(bo await base.Select_subquery_int_with_outside_cast_and_coalesce(async); AssertSql( - @"SELECT COALESCE(( + @"SELECT COALESCE(COALESCE(( SELECT TOP(1) [w].[Id] FROM [Weapons] AS [w] WHERE [g].[FullName] = [w].[OwnerFullName] - ORDER BY [w].[Id]), 42) + ORDER BY [w].[Id]), 0), 42) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6516,11 +6508,11 @@ public override async Task Select_subquery_boolean_empty(bool async) await base.Select_subquery_boolean_empty(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) + ORDER BY [w].[Id]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } @@ -6539,46 +6531,18 @@ FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); } - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable1(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[IsAutomatic] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g] -LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); - } - - public override async Task Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(bool async) - { - await base.Select_subquery_boolean_empty_with_pushdown_without_convert_to_nullable2(async); - - AssertSql( - @"SELECT ( - SELECT TOP(1) [w].[Id] - FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ORDER BY [w].[Id]) -FROM [Gears] AS [g] -LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId])"); - } - public override async Task Select_subquery_distinct_singleordefault_boolean1(bool async) { await base.Select_subquery_distinct_singleordefault_boolean1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6589,10 +6553,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean2(boo await base.Select_subquery_distinct_singleordefault_boolean2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] LIKE N'%Lancer%')), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6620,13 +6584,13 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty1(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT TOP(1) [t].[IsAutomatic] FROM ( SELECT DISTINCT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG') - ) AS [t]) + ) AS [t]), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); @@ -6637,10 +6601,10 @@ public override async Task Select_subquery_distinct_singleordefault_boolean_empt await base.Select_subquery_distinct_singleordefault_boolean_empty2(async); AssertSql( - @"SELECT ( + @"SELECT COALESCE(( SELECT DISTINCT TOP(1) [w].[IsAutomatic] FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')) + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[Name] = N'BFG')), CAST(0 AS bit)) FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) WHERE [g].[HasSoulPatch] = CAST(1 AS bit)"); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs index 769e2d04487..88400ee7310 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/SpatialQuerySqliteTest.cs @@ -256,19 +256,6 @@ public override async Task Disjoint_with_cast_to_nullable(bool async) AssertSql( @"@__point_0='0x000100000000000000000000F03F000000000000F03F000000000000F03F0000...' (Size = 60) (DbType = String) -SELECT ""p"".""Id"", CASE - WHEN ""p"".""Polygon"" IS NOT NULL THEN Disjoint(""p"".""Polygon"", @__point_0) -END AS ""Disjoint"" -FROM ""PolygonEntity"" AS ""p"""); - } - - public override async Task Disjoint_without_cast_to_nullable(bool async) - { - await base.Disjoint_without_cast_to_nullable(async); - - AssertSql( - @"@__point_0='0x000100000000000000000000F03F000000000000F03F000000000000F03F0000...' (Size = 60) (DbType = String) - SELECT ""p"".""Id"", CASE WHEN ""p"".""Polygon"" IS NOT NULL THEN Disjoint(""p"".""Polygon"", @__point_0) END AS ""Disjoint"" @@ -289,17 +276,6 @@ END AS ""Disjoint"" FROM ""PolygonEntity"" AS ""p"""); } - public override async Task Distance_without_null_check(bool async) - { - await base.Distance_without_null_check(async); - - AssertSql( - @"@__point_0='0x0001000000000000000000000000000000000000F03F00000000000000000000...' (Size = 60) (DbType = String) - -SELECT ""p"".""Id"", Distance(""p"".""Point"", @__point_0) AS ""Distance"" -FROM ""PointEntity"" AS ""p"""); - } - public override async Task Distance_with_null_check(bool async) { await base.Distance_with_null_check(async); diff --git a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs index 1faa3027bdd..eb444c1c398 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs @@ -477,6 +477,34 @@ public void Cannot_remove_manually_created_association_entity_type(bool removeSk Assert.Same(manyToManyJoin.Metadata, leftSkipNav.AssociationEntityType); } + [ConditionalFact] + public void Can_add_shared_type() + { + var model = new Model(); + var modelBuilder = CreateModelBuilder(model); + + var entityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + var sharedTypeName = "SpecialDetails"; + + Assert.NotNull(modelBuilder.SharedEntity(sharedTypeName, typeof(Details), ConfigurationSource.Convention)); + + Assert.True(model.FindEntityType(sharedTypeName).HasSharedClrType); + + Assert.Equal( + CoreStrings.ClashingMismatchedSharedType("SpecialDetails"), + Assert.Throws(() => modelBuilder.SharedEntity(sharedTypeName, typeof(Product), ConfigurationSource.DataAnnotation)).Message); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.Null(modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.Explicit)); + + Assert.Equal( + CoreStrings.ClashingNonSharedType(typeof(Product).DisplayName()), + Assert.Throws(() => modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.Explicit)).Message); + } + private static void Cleanup(InternalModelBuilder modelBuilder) { new ModelCleanupConvention(CreateDependencies()) diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index ad4c4b67d1b..1fe4340310b 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; @@ -356,6 +357,107 @@ public virtual void Navigation_properties_can_set_access_mode_using_navigation_n Assert.Equal(PropertyAccessMode.Field, principal.FindSkipNavigation("Dependents").GetPropertyAccessMode()); Assert.Equal(PropertyAccessMode.Property, dependent.FindSkipNavigation("ManyToManyPrincipals").GetPropertyAccessMode()); } + + [ConditionalFact] + public virtual void Can_use_shared_Type_as_join_entity() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared1", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany()); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared2", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany(), + e => e.IndexerProperty("Payload")); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.Equal(2, shared1.GetForeignKeys().Count()); + Assert.True(shared1.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared1.ClrType); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.Equal(2, shared2.GetForeignKeys().Count()); + Assert.True(shared2.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared2.ClrType); + Assert.NotNull(shared2.FindProperty("Payload")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_fails_when_not_marked() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + CoreStrings.TypeNotMarkedAsShared(typeof(ManyToManyJoinWithFields).DisplayName()), + Assert.Throws( + () => modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany())).Message); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_passed_when_marked_as_shared_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.SharedEntity(); + + var associationEntityType = modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany()).Metadata; + + Assert.True(associationEntityType.HasSharedClrType); + Assert.Equal("Shared", associationEntityType.Name); + Assert.Equal(typeof(ManyToManyJoinWithFields), associationEntityType.ClrType); + } + + [ConditionalFact] + public virtual void UsingEntity_with_shared_type_passes_when_configured_as_shared() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.SharedEntity("Shared"); + + var associationEntityType = modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany()).Metadata; + + Assert.True(associationEntityType.HasSharedClrType); + Assert.Equal("Shared", associationEntityType.Name); + Assert.Equal(typeof(ManyToManyJoinWithFields), associationEntityType.ClrType); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs index a26a57a3d29..9f81aa0d2d7 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs @@ -48,6 +48,9 @@ public GenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericStringTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -56,8 +59,19 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); public override TestModelBuilder Ignore() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs index d3b08524a5b..37527c5178b 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs @@ -29,6 +29,9 @@ public GenericTypeTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTypeTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTypeTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -37,8 +40,19 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTypeTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); public override TestModelBuilder Ignore() { diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 67be90429d6..cfde1fa3e94 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -127,6 +127,11 @@ public GenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestSharedEntityTypeBuilder SharedEntity() + => new GenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity()); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -135,6 +140,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); @@ -349,6 +363,20 @@ public GenericTestOwnedEntityTypeBuilder(OwnedEntityTypeBuilder ownedEn public OwnedEntityTypeBuilder Instance => OwnedEntityTypeBuilder; } + protected class GenericTestSharedEntityTypeBuilder : TestSharedEntityTypeBuilder, + IInfrastructure> + where TEntity : class + { + public GenericTestSharedEntityTypeBuilder(SharedEntityTypeBuilder sharedEntityTypeBuilder) + { + SharedEntityTypeBuilder = sharedEntityTypeBuilder; + } + + protected SharedEntityTypeBuilder SharedEntityTypeBuilder { get; } + + public SharedEntityTypeBuilder Instance => SharedEntityTypeBuilder; + } + protected class GenericTestPropertyBuilder : TestPropertyBuilder, IInfrastructure> { public GenericTestPropertyBuilder(PropertyBuilder propertyBuilder) @@ -652,7 +680,35 @@ public override TestEntityTypeBuilder UsingEntity ((GenericTestReferenceCollectionBuilder)configureLeft( new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new GenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -660,6 +716,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, l => ((GenericTestReferenceCollectionBuilder)configureRight( new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, r => ((GenericTestReferenceCollectionBuilder)configureLeft( diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs index 4c491d10c51..3dc031a1748 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs @@ -81,6 +81,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -89,9 +92,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), + e => buildAction(new NonGenericStringTestEntityTypeBuilder(e))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index c01283d2765..8dc7fe1aa4f 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -75,6 +75,9 @@ public NonGenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -83,9 +86,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); @@ -332,6 +347,20 @@ public NonGenericTestOwnedEntityTypeBuilder(OwnedEntityTypeBuilder ownedEntityTy public OwnedEntityTypeBuilder Instance => OwnedEntityTypeBuilder; } + protected class NonGenericTestSharedEntityTypeBuilder : TestSharedEntityTypeBuilder, + IInfrastructure + where TEntity : class + { + public NonGenericTestSharedEntityTypeBuilder(SharedEntityTypeBuilder sharedEntityTypeBuilder) + { + SharedEntityTypeBuilder = sharedEntityTypeBuilder; + } + + protected SharedEntityTypeBuilder SharedEntityTypeBuilder { get; } + + public SharedEntityTypeBuilder Instance => SharedEntityTypeBuilder; + } + protected class NonGenericTestPropertyBuilder : TestPropertyBuilder, IInfrastructure { public NonGenericTestPropertyBuilder(PropertyBuilder propertyBuilder) @@ -646,7 +675,37 @@ public override TestEntityTypeBuilder UsingEntity ((NonGenericTestReferenceCollectionBuilder)configureLeft( new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new NonGenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -654,6 +713,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, typeof(TAssociationEntity), l => ((NonGenericTestReferenceCollectionBuilder)configureRight( new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs index bba78ade724..7bcb6edeff0 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs @@ -30,6 +30,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -38,9 +41,21 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); + public override TestSharedEntityTypeBuilder SharedEntity() + => new NonGenericTestSharedEntityTypeBuilder(ModelBuilder.SharedEntity(typeof(TEntity))); + public override TestModelBuilder Ignore() { ModelBuilder.Ignore(typeof(TEntity)); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index dd8214379d8..dca02421aa1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -148,12 +148,21 @@ public TestModelBuilder HasAnnotation(string annotation, object value) public abstract TestEntityTypeBuilder Entity() where TEntity : class; + public abstract TestEntityTypeBuilder SharedEntity(string name) + where TEntity : class; + public abstract TestOwnedEntityTypeBuilder Owned() where TEntity : class; + public abstract TestSharedEntityTypeBuilder SharedEntity() + where TEntity : class; + public abstract TestModelBuilder Entity(Action> buildAction) where TEntity : class; + public abstract TestModelBuilder SharedEntity(string name, Action> buildAction) + where TEntity : class; + public abstract TestModelBuilder Ignore() where TEntity : class; @@ -290,6 +299,10 @@ public abstract class TestOwnedEntityTypeBuilder where TEntity : class { } + public abstract class TestSharedEntityTypeBuilder + where TEntity : class + { + } public abstract class TestKeyBuilder { @@ -444,7 +457,24 @@ public abstract TestEntityTypeBuilder UsingEntity> configureLeft) where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + where TAssociationEntity : class; + + public abstract TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index d80328bb7d8..707ffbe6316 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1540,6 +1540,44 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property_diction Assert.Equal(2, data["Required"]); Assert.False(data.ContainsKey("Optional")); } + + [ConditionalFact] + public virtual void Can_add_shared_type_entity_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.SharedEntity>("Shared1"); + + modelBuilder.SharedEntity>("Shared2", b => b.IndexerProperty("Id")); + + var model = modelBuilder.Model; + Assert.Equal(2, model.GetEntityTypes().Count()); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.True(shared1.HasSharedClrType); + Assert.Null(shared1.FindProperty("Id")); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.True(shared2.HasSharedClrType); + Assert.NotNull(shared2.FindProperty("Id")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void Cannot_add_shared_type_when_non_shared_exists() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.ClashingNonSharedType("Shared1"), + Assert.Throws(() => modelBuilder.SharedEntity("Shared1")).Message); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index a30f95f0d6d..ec74f9c8180 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -144,7 +144,6 @@ protected class Order : INotifyPropertyChanged public OrderCombination OrderCombination { get; set; } public OrderDetails Details { get; set; } public ICollection Products { get; set; } - public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { @@ -930,7 +929,7 @@ private class OwnedOneToManyNavDependent public OneToManyNavPrincipalOwner OneToManyOwner { get; set; } } - public class OwnerOfOwnees + protected class OwnerOfOwnees { public string Id { get; private set; } @@ -938,22 +937,22 @@ public class OwnerOfOwnees public Ownee1 Ownee1 { get; private set; } } - public class Ownee1 + protected class Ownee1 { public Ownee3 NewOwnee3 { get; private set; } } - public class Ownee2 + protected class Ownee2 { public Ownee3 Ownee3 { get; private set; } } - public class Ownee3 + protected class Ownee3 { public string Name { get; private set; } } - public class OneToManyPrincipalWithField + protected class OneToManyPrincipalWithField { public int Id; public Guid AlternateKey; @@ -962,7 +961,7 @@ public class OneToManyPrincipalWithField public IEnumerable Dependents; } - public class OneToOnePrincipalWithField + protected class OneToOnePrincipalWithField { public int Id; public string Name; @@ -970,7 +969,7 @@ public class OneToOnePrincipalWithField public DependentWithField Dependent; } - public class ManyToManyPrincipalWithField + protected class ManyToManyPrincipalWithField { public int Id; public string Name; @@ -987,7 +986,7 @@ protected class ManyToManyJoinWithFields public DependentWithField DependentWithField { get; set; } } - public class DependentWithField + protected class DependentWithField { public int DependentWithFieldId; @@ -999,7 +998,7 @@ public class DependentWithField public List ManyToManyPrincipals { get; set; } } - public class OneToManyOwnerWithField + protected class OneToManyOwnerWithField { public int Id; public Guid AlternateKey; @@ -1008,7 +1007,7 @@ public class OneToManyOwnerWithField public List OwnedDependents { get; set; } } - public class OneToManyOwnedWithField + protected class OneToManyOwnedWithField { public string FirstName; public string LastName; @@ -1017,7 +1016,7 @@ public class OneToManyOwnedWithField public OneToManyOwnerWithField OneToManyOwner { get; set; } } - public class OneToOneOwnerWithField + protected class OneToOneOwnerWithField { public int Id; public Guid AlternateKey; @@ -1026,7 +1025,7 @@ public class OneToOneOwnerWithField public OneToOneOwnedWithField OwnedDependent { get; set; } } - public class OneToOneOwnedWithField + protected class OneToOneOwnedWithField { public string FirstName; public string LastName; @@ -1036,7 +1035,7 @@ public class OneToOneOwnedWithField } - public class ImplicitManyToManyA + protected class ImplicitManyToManyA { public int Id { get; set; } public string Name { get; set; } @@ -1045,12 +1044,55 @@ public class ImplicitManyToManyA } - public class ImplicitManyToManyB + protected class ImplicitManyToManyB { public int Id { get; set; } public string Name { get; set; } public List As { get; set; } } + + protected class SharedHolderAlpha + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedHolderBeta + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedTypeEntityType + { + [NotMapped] + public int Random { get; set; } + [NotMapped] + public SharedNestedOwnedEntityType NestedReference { get; set; } + [NotMapped] + public List NestedCollection { get; set; } + [NotMapped] + public NestedReference ReferenceNavigation { get; set; } + } + + protected class SharedNestedOwnedEntityType + { + [NotMapped] + public int NestedRandom { get; set; } + } + + protected class NestedReference + { + public int Id { get; set; } + [NotMapped] + public string Value { get; set; } + } } }