From c1392d615fffb40eaeb451ee9e6d5c1732fdb348 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Mon, 12 Aug 2019 18:02:35 -0700 Subject: [PATCH] Query: Preseve constant based naked Initialization expressions in expression tree This puts some of the processing (evaluating the expression to the corresponding constant) inside translation pipeline. - When applying EntityEquality, assumption here is that property is always going to be mapped on server side so we can generate constant. - When translating newExpression. If the generated constant can be mapped, it would work. (like new Datetime()) else translation would null out. Resolves #15712 Resolves #17048 Resolves #7983 --- .../CosmosSqlTranslatingExpressionVisitor.cs | 43 +++++++++- .../Query/Internal/QuerySqlGenerator.cs | 9 ++- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ...lationalSqlTranslatingExpressionVisitor.cs | 46 +++++++++-- ...ntityEqualityRewritingExpressionVisitor.cs | 27 +++++++ ...ryableMethodConvertingExpressionVisitor.cs | 12 ++- .../ParameterExtractingExpressionVisitor.cs | 12 ++- .../Query/ReplacingExpressionVisitor.cs | 15 ++-- .../Query/SimpleQueryCosmosTest.Select.cs | 1 + .../Query/GearsOfWarQueryTestBase.cs | 4 +- .../Query/GroupByQueryTestBase.cs | 2 +- .../Query/SimpleQueryTestBase.Select.cs | 8 +- .../Query/SimpleQueryTestBase.cs | 2 +- .../Query/GroupByQuerySqlServerTest.cs | 16 +--- .../Query/QueryBugsTest.cs | 78 +++++++++++++++++++ 15 files changed, 230 insertions(+), 47 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index f6bcdb51d2d..3a91054a6f3 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -58,6 +58,14 @@ public virtual SqlExpression Translate(Expression expression) { translation = _sqlExpressionFactory.ApplyDefaultTypeMapping(translation); + if ((translation is SqlConstantExpression + || translation is SqlParameterExpression) + && translation.TypeMapping == null) + { + // Non-mappable constant/parameter + return null; + } + _sqlVerifyingExpressionVisitor.Visit(translation); return translation; @@ -339,13 +347,44 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return null; } + private SqlConstantExpression GetConstantOrNull(Expression expression) + { + if (CanEvaluate(expression)) + { + var value = Expression.Lambda>(Expression.Convert(expression, typeof(object))).Compile().Invoke(); + return new SqlConstantExpression(Expression.Constant(value, expression.Type), null); + } + + return null; + } + + private static bool CanEvaluate(Expression expression) + { + switch (expression) + { + case ConstantExpression constantExpression: + return true; + + case NewExpression newExpression: + return newExpression.Arguments.All(e => CanEvaluate(e)); + + case MemberInitExpression memberInitExpression: + return CanEvaluate(memberInitExpression.NewExpression) + && memberInitExpression.Bindings.All( + mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression)); + + default: + return false; + } + } + /// /// 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 node) => null; + protected override Expression VisitNew(NewExpression node) => GetConstantOrNull(node); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -353,7 +392,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) /// 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 VisitMemberInit(MemberInitExpression node) => null; + protected override Expression VisitMemberInit(MemberInitExpression node) => GetConstantOrNull(node); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs index 43f8c6fbe2b..532d39beec4 100644 --- a/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/QuerySqlGenerator.cs @@ -179,7 +179,14 @@ protected override Expression VisitSelect(SelectExpression selectExpression) _sqlBuilder.Append("DISTINCT "); } - GenerateList(selectExpression.Projection, t => Visit(t)); + if (selectExpression.Projection.Any()) + { + GenerateList(selectExpression.Projection, e => Visit(e)); + } + else + { + _sqlBuilder.Append("1"); + } _sqlBuilder.AppendLine(); _sqlBuilder.Append("FROM root "); diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 26c463f5a4d..09651739f37 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -302,7 +302,7 @@ protected override ShapedQueryExpression TranslateGroupBy( var remappedKeySelector = RemapLambdaBody(source, keySelector); var translatedKey = TranslateGroupingKey(remappedKeySelector) - ?? (remappedKeySelector is ConstantExpression ? remappedKeySelector : null); + ?? (remappedKeySelector as ConstantExpression); if (translatedKey != null) { if (elementSelector != null) diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 082f2c669ee..72b481c59c7 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -19,7 +19,7 @@ public class RelationalSqlTranslatingExpressionVisitor : ExpressionVisitor private readonly IModel _model; private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor; private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlVerifyingExpressionVisitor; + private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlTypeMappingVerifyingExpressionVisitor; public RelationalSqlTranslatingExpressionVisitor( RelationalSqlTranslatingExpressionVisitorDependencies dependencies, @@ -31,7 +31,7 @@ public RelationalSqlTranslatingExpressionVisitor( _model = model; _queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor; _sqlExpressionFactory = dependencies.SqlExpressionFactory; - _sqlVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); + _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); } protected virtual RelationalSqlTranslatingExpressionVisitorDependencies Dependencies { get; } @@ -51,14 +51,15 @@ public virtual SqlExpression Translate(Expression expression) translation = _sqlExpressionFactory.ApplyDefaultTypeMapping(translation); - if (translation is SqlConstantExpression + if ((translation is SqlConstantExpression + || translation is SqlParameterExpression) && translation.TypeMapping == null) { - // Non-mappable constant + // Non-mappable constant/parameter return null; } - _sqlVerifyingExpressionVisitor.Visit(translation); + _sqlTypeMappingVerifyingExpressionVisitor.Visit(translation); return translation; } @@ -442,9 +443,40 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) null); } - protected override Expression VisitNew(NewExpression node) => null; + private SqlConstantExpression GetConstantOrNull(Expression expression) + { + if (CanEvaluate(expression)) + { + var value = Expression.Lambda>(Expression.Convert(expression, typeof(object))).Compile().Invoke(); + return new SqlConstantExpression(Expression.Constant(value, expression.Type), null); + } + + return null; + } + + private static bool CanEvaluate(Expression expression) + { + switch (expression) + { + case ConstantExpression constantExpression: + return true; + + case NewExpression newExpression: + return newExpression.Arguments.All(e => CanEvaluate(e)); + + case MemberInitExpression memberInitExpression: + return CanEvaluate(memberInitExpression.NewExpression) + && memberInitExpression.Bindings.All( + mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression)); + + default: + return false; + } + } + + protected override Expression VisitNew(NewExpression node) => GetConstantOrNull(node); - protected override Expression VisitMemberInit(MemberInitExpression node) => null; + protected override Expression VisitMemberInit(MemberInitExpression node) => GetConstantOrNull(node); protected override Expression VisitNewArray(NewArrayExpression node) => null; diff --git a/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs b/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs index 0b9f4560827..1872552558a 100644 --- a/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs @@ -914,6 +914,13 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p return Expression.Constant(property.GetGetter().GetClrValue(constantExpression.Value), property.ClrType.MakeNullable()); } + // The target is complex which can be evaluated to Constant. + if (CanEvaluate(target)) + { + var value = Expression.Lambda>(Expression.Convert(target, typeof(object))).Compile().Invoke(); + return Expression.Constant(property.GetGetter().GetClrValue(value), property.ClrType.MakeNullable()); + } + // If the target is a query parameter, we can't simply add a property access over it, but must instead cause a new // parameter to be added at runtime, with the value of the property on the base parameter. if (target is ParameterExpression baseParameterExpression @@ -936,6 +943,26 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p return target.CreateEFPropertyExpression(property, true); } + private static bool CanEvaluate(Expression expression) + { + switch (expression) + { + case ConstantExpression constantExpression: + return true; + + case NewExpression newExpression: + return newExpression.Arguments.All(e => CanEvaluate(e)); + + case MemberInitExpression memberInitExpression: + return CanEvaluate(memberInitExpression.NewExpression) + && memberInitExpression.Bindings.All( + mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression)); + + default: + return false; + } + } + private static object ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.ParameterValues[baseParameterName]; diff --git a/src/EFCore/Query/Internal/EnumerableToQueryableMethodConvertingExpressionVisitor.cs b/src/EFCore/Query/Internal/EnumerableToQueryableMethodConvertingExpressionVisitor.cs index da04eb781f3..291104dfcee 100644 --- a/src/EFCore/Query/Internal/EnumerableToQueryableMethodConvertingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/EnumerableToQueryableMethodConvertingExpressionVisitor.cs @@ -25,8 +25,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } if (methodCallExpression.Arguments.Count > 0 - && (methodCallExpression.Arguments[0] is ParameterExpression - || methodCallExpression.Arguments[0] is ConstantExpression)) + && ClientSource(methodCallExpression.Arguments[0])) { // this is methodCall over closure variable or constant return base.VisitMethodCall(methodCallExpression); @@ -137,8 +136,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Method.DeclaringType.GetGenericTypeDefinition() == typeof(List<>) && string.Equals(nameof(List.Contains), methodCallExpression.Method.Name)) { - if (methodCallExpression.Object is ParameterExpression - || methodCallExpression.Object is ConstantExpression) + if (ClientSource(methodCallExpression.Object)) { // this is methodCall over closure variable or constant return base.VisitMethodCall(methodCallExpression); @@ -157,6 +155,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return base.VisitMethodCall(methodCallExpression); } + private static bool ClientSource(Expression expression) + => expression is ConstantExpression + || expression is MemberInitExpression + || expression is NewExpression + || expression is ParameterExpression; + private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType) { if (enumerableType == typeof(IEnumerable) diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs index f7e0ca42b28..f937da0e46e 100644 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs @@ -100,6 +100,7 @@ public override Expression Visit(Expression expression) } if (_evaluatableExpressions.TryGetValue(expression, out var generateParameter) + && !PreserveInitializationConstant(expression, generateParameter) && !PreserveConvertNode(expression)) { return Evaluate(expression, _parameterize && generateParameter); @@ -108,13 +109,10 @@ public override Expression Visit(Expression expression) return base.Visit(expression); } - /// - /// 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 virtual bool PreserveConvertNode(Expression expression) + private bool PreserveInitializationConstant(Expression expression, bool generateParameter) + => !generateParameter && (expression is NewExpression || expression is MemberInitExpression); + + private bool PreserveConvertNode(Expression expression) { if (expression is UnaryExpression unaryExpression && (unaryExpression.NodeType == ExpressionType.Convert diff --git a/src/EFCore/Query/ReplacingExpressionVisitor.cs b/src/EFCore/Query/ReplacingExpressionVisitor.cs index a62a9f75fb1..287d3910468 100644 --- a/src/EFCore/Query/ReplacingExpressionVisitor.cs +++ b/src/EFCore/Query/ReplacingExpressionVisitor.cs @@ -72,15 +72,20 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var newEntityExpression = Visit(entityExpression); if (newEntityExpression is NewExpression newExpression) { - var index = newExpression.Members.Select(m => m.Name).IndexOf(propertyName); - - return newExpression.Arguments[index]; + var index = newExpression.Members?.Select(m => m.Name).IndexOf(propertyName); + if (index > 0) + { + return newExpression.Arguments[index.Value]; + } } if (newEntityExpression is MemberInitExpression memberInitExpression) { - return ((MemberAssignment)memberInitExpression.Bindings - .Single(mb => mb.Member.Name == propertyName)).Expression; + if (memberInitExpression.Bindings.SingleOrDefault( + mb => mb.Member.Name == propertyName) is MemberAssignment memberAssignment) + { + return memberAssignment.Expression; + } } return methodCallExpression.Update(null, new[] { newEntityExpression, methodCallExpression.Arguments[1] }); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.Select.cs b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.Select.cs index 926a4aac751..fe3ff81016a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.Select.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/SimpleQueryCosmosTest.Select.cs @@ -364,6 +364,7 @@ FROM root c WHERE (c[""Discriminator""] = ""Customer"")"); } + [ConditionalTheory(Skip = "Issue#14935")] public override async Task New_date_time_in_anonymous_type_works(bool isAsync) { await base.New_date_time_in_anonymous_type_works(isAsync); diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 0e8d42d9841..ba1a64adc78 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -6259,7 +6259,7 @@ public virtual Task Select_subquery_projecting_single_constant_bool(bool isAsync })); } - [ConditionalTheory(Skip = "issue #15712")] + [ConditionalTheory(Skip = "Issue#10001")] [MemberData(nameof(IsAsyncData))] public virtual Task Select_subquery_projecting_single_constant_inside_anonymous(bool isAsync) { @@ -6277,7 +6277,7 @@ public virtual Task Select_subquery_projecting_single_constant_inside_anonymous( })); } - [ConditionalTheory(Skip = "issue #15712")] + [ConditionalTheory(Skip = "Issue#10001")] [MemberData(nameof(IsAsyncData))] public virtual Task Select_subquery_projecting_multiple_constants_inside_anonymous(bool isAsync) { diff --git a/test/EFCore.Specification.Tests/Query/GroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GroupByQueryTestBase.cs index 191f4816440..20582d87c9b 100644 --- a/test/EFCore.Specification.Tests/Query/GroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GroupByQueryTestBase.cs @@ -1277,7 +1277,7 @@ public virtual Task GroupBy_empty_key_Aggregate(bool isAsync) .Select(g => g.Sum(o => o.OrderID))); } - [ConditionalTheory(Skip = "Issue#17048")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_empty_key_Aggregate_Key(bool isAsync) { diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs index aed74cbb086..d8c3ac7bce0 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.Select.cs @@ -101,7 +101,7 @@ private static void AssertArrays(object e, object a, int count) } } - [ConditionalTheory(Skip = "Issue #15712")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Select_bool_closure(bool isAsync) { @@ -309,7 +309,7 @@ public virtual Task Select_anonymous_nested(bool isAsync) e => e.City); } - [ConditionalTheory(Skip = "Issue#15712")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_anonymous_empty(bool isAsync) { @@ -322,7 +322,7 @@ public virtual Task Select_anonymous_empty(bool isAsync) e => 1); } - [ConditionalTheory(Skip = "Issue#15712")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_anonymous_literal(bool isAsync) { @@ -620,7 +620,7 @@ orderby o2.OrderID }); } - [ConditionalTheory(Skip = "Issue#15712")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task New_date_time_in_anonymous_type_works(bool isAsync) { diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs index c7eb6a3150e..fbb114df3dd 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs @@ -4308,7 +4308,7 @@ public virtual Task Select_expression_int_to_string(bool isAsync) e => e.ShipName); } - [ConditionalTheory(Skip = "Issue#17048")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task ToString_with_formatter_is_evaluated_on_the_client(bool isAsync) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GroupByQuerySqlServerTest.cs index 6ebdce2774f..8aaf85c2718 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GroupByQuerySqlServerTest.cs @@ -824,12 +824,8 @@ public override async Task GroupBy_empty_key_Aggregate(bool isAsync) await base.GroupBy_empty_key_Aggregate(isAsync); AssertSql( - @"SELECT SUM([t].[OrderID]) -FROM ( - SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], 1 AS [Key] - FROM [Orders] AS [o] -) AS [t] -GROUP BY [t].[Key]"); + @"SELECT SUM([o].[OrderID]) +FROM [Orders] AS [o]"); } public override async Task GroupBy_empty_key_Aggregate_Key(bool isAsync) @@ -837,12 +833,8 @@ public override async Task GroupBy_empty_key_Aggregate_Key(bool isAsync) await base.GroupBy_empty_key_Aggregate_Key(isAsync); AssertSql( - @"SELECT SUM([t].[OrderID]) -FROM ( - SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], 1 AS [Key] - FROM [Orders] AS [o] -) AS [t] -GROUP BY [t].[Key]"); + @"SELECT SUM([o].[OrderID]) AS [Sum] +FROM [Orders] AS [o]"); } public override async Task OrderBy_GroupBy_Aggregate(bool isAsync) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index b359589318d..f0c401ef85b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6132,6 +6132,84 @@ public class Customer8864 #endregion + #region Bug7983 + + [ConditionalFact] + public virtual void New_instances_in_projection_are_not_shared_across_results() + { + using (CreateDatabase7983()) + { + using (var context = new MyContext7983(_options)) + { + var list = context.Posts.Select(p => new PostDTO7983().From(p)).ToList(); + + Assert.Equal(3, list.Count); + Assert.Equal(new[] { "First", "Second", "Third" }, list.Select(dto => dto.Title)); + + AssertSql( + @"SELECT [p].[Id], [p].[BlogId], [p].[Title] +FROM [Posts] AS [p]"); + } + } + } + + private SqlServerTestStore CreateDatabase7983() + => CreateTestStore( + () => new MyContext7983(_options), + context => + { + context.Add(new Blog7983 + { + Posts = new List { + new Post7983 { Title = "First" }, + new Post7983 { Title = "Second" }, + new Post7983 { Title = "Third" } } + }); + + context.SaveChanges(); + + ClearLog(); + }); + + public class MyContext7983 : DbContext + { + public DbSet Blogs { get; set; } + public DbSet Posts { get; set; } + + public MyContext7983(DbContextOptions options) : base(options) + { + } + } + + public class Blog7983 + { + public int Id { get; set; } + public string Title { get; set; } + + public ICollection Posts { get; set; } + } + + public class Post7983 + { + public int Id { get; set; } + public string Title { get; set; } + + public int? BlogId { get; set; } + public Blog7983 Blog { get; set; } + } + + public class PostDTO7983 + { + public string Title { get; set; } + public PostDTO7983 From(Post7983 post) + { + Title = post.Title; + return this; + } + } + + #endregion + private DbContextOptions _options; private SqlServerTestStore CreateTestStore(