From c11921824c00f4e435b48d24647584a5efae0e11 Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Sun, 2 Nov 2025 21:57:40 +0100 Subject: [PATCH 1/4] Handle implicit convert expression in LINQ update with anonymous type. --- .../Issues/GH3716/Entity.vb | 7 +++ .../Issues/GH3716/Fixture.vb | 58 +++++++++++++++++++ .../GH3716/Issues.GH3716.Mappings.hbm.xml | 11 ++++ src/NHibernate/Linq/DmlExpressionRewriter.cs | 14 +++-- 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 src/NHibernate.Test.VisualBasic/Issues/GH3716/Entity.vb create mode 100644 src/NHibernate.Test.VisualBasic/Issues/GH3716/Fixture.vb create mode 100644 src/NHibernate.Test.VisualBasic/Issues/GH3716/Issues.GH3716.Mappings.hbm.xml diff --git a/src/NHibernate.Test.VisualBasic/Issues/GH3716/Entity.vb b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Entity.vb new file mode 100644 index 00000000000..4fcdb0841e5 --- /dev/null +++ b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Entity.vb @@ -0,0 +1,7 @@ +Namespace Issues.GH3716 + Public Class Entity + Public Overridable Property Id As Guid + + Public Overridable Property Date1 As Date? + End Class +End Namespace diff --git a/src/NHibernate.Test.VisualBasic/Issues/GH3716/Fixture.vb b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Fixture.vb new file mode 100644 index 00000000000..cff78cfc770 --- /dev/null +++ b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Fixture.vb @@ -0,0 +1,58 @@ +Imports NHibernate.Linq +Imports NUnit.Framework + +Namespace Issues.GH3716 + + Public Class Fixture + Inherits IssueTestCase + + Protected Overrides Sub OnSetUp() + + Using session As ISession = OpenSession() + + Using transaction As ITransaction = session.BeginTransaction() + + Dim e1 = New Entity + e1.Date1 = New Date(2017, 12, 3) + session.Save(e1) + + Dim e2 = New Entity + e2.Date1 = New Date(2017, 12, 1) + session.Save(e2) + + Dim e3 = New Entity + session.Save(e3) + + session.Flush() + transaction.Commit() + + End Using + + End Using + End Sub + + Protected Overrides Sub OnTearDown() + + Using session As ISession = OpenSession() + + Using transaction As ITransaction = session.BeginTransaction() + + session.Delete("from System.Object") + + session.Flush() + transaction.Commit() + + End Using + + End Using + End Sub + + + Public Sub ShouldBeAbleToUpdateWithAnonymousType() + + Using session As ISession = OpenSession() + session.Query(Of Entity).Update(Function(x) New With {.Date1 = Date.Today}) + End Using + End Sub + End Class +End Namespace diff --git a/src/NHibernate.Test.VisualBasic/Issues/GH3716/Issues.GH3716.Mappings.hbm.xml b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Issues.GH3716.Mappings.hbm.xml new file mode 100644 index 00000000000..5ce0f339870 --- /dev/null +++ b/src/NHibernate.Test.VisualBasic/Issues/GH3716/Issues.GH3716.Mappings.hbm.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 782318a64a7..2bee0c7865e 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -25,10 +25,10 @@ void AddSettersFromBindings(IEnumerable bindings, string path) switch (node.BindingType) { case MemberBindingType.Assignment: - AddSettersFromAssignment((MemberAssignment)node, subPath); + AddSettersFromAssignment((MemberAssignment) node, subPath); break; case MemberBindingType.MemberBinding: - AddSettersFromBindings(((MemberMemberBinding)node).Bindings, subPath); + AddSettersFromBindings(((MemberMemberBinding) node).Bindings, subPath); break; default: throw new InvalidOperationException($"{node.BindingType} is not supported"); @@ -53,10 +53,10 @@ void AddSettersFromAnonymousConstructor(NewExpression newExpression, string path switch (argument.NodeType) { case ExpressionType.New: - AddSettersFromAnonymousConstructor((NewExpression)argument, subPath); + AddSettersFromAnonymousConstructor((NewExpression) argument, subPath); break; case ExpressionType.MemberInit: - AddSettersFromBindings(((MemberInitExpression)argument).Bindings, subPath); + AddSettersFromBindings(((MemberInitExpression) argument).Bindings, subPath); break; default: _assignments.Add(subPath.Substring(1), Expression.Lambda(argument, _parameters)); @@ -121,9 +121,11 @@ public static Expression PrepareExpressionFromAnonymous(Expression sour if (expression == null) throw new ArgumentNullException(nameof(expression)); - // Anonymous initializations are not implemented as member initialization but as plain constructor call. + // Anonymous initializations are not implemented as member initialization but as plain constructor call, potentially wrapped in a Convert expression var newExpression = expression.Body as NewExpression ?? - throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); + (expression.Body is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Operand is NewExpression newExpressionOperand + ? newExpressionOperand + : throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }")); var instance = new DmlExpressionRewriter(expression.Parameters); instance.AddSettersFromAnonymousConstructor(newExpression, ""); From 6621c5e0bdbb9c9f1b0e66f23c9e3025212fda9a Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 3 Nov 2025 10:32:12 +1000 Subject: [PATCH 2/4] Microsoft.VisualBasic assembly is not required anymore --- .../NHibernate.Test.VisualBasic.vbproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NHibernate.Test.VisualBasic/NHibernate.Test.VisualBasic.vbproj b/src/NHibernate.Test.VisualBasic/NHibernate.Test.VisualBasic.vbproj index 5ea684f06b2..ff258df5705 100644 --- a/src/NHibernate.Test.VisualBasic/NHibernate.Test.VisualBasic.vbproj +++ b/src/NHibernate.Test.VisualBasic/NHibernate.Test.VisualBasic.vbproj @@ -26,7 +26,6 @@ - From 2d806b06ebb798e219b175f0a66f21932b66ee3c Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 3 Nov 2025 10:40:48 +1000 Subject: [PATCH 3/4] Cleanup code --- src/NHibernate/Linq/DmlExpressionRewriter.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 2bee0c7865e..6f2b5df3c4d 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -122,16 +122,25 @@ public static Expression PrepareExpressionFromAnonymous(Expression sour throw new ArgumentNullException(nameof(expression)); // Anonymous initializations are not implemented as member initialization but as plain constructor call, potentially wrapped in a Convert expression - var newExpression = expression.Body as NewExpression ?? - (expression.Body is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Operand is NewExpression newExpressionOperand - ? newExpressionOperand - : throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }")); + var newExpression = UnwrapConvert(expression.Body) as NewExpression ?? + throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); var instance = new DmlExpressionRewriter(expression.Parameters); instance.AddSettersFromAnonymousConstructor(newExpression, ""); return PrepareExpression(sourceExpression, instance._assignments); } + private static Expression UnwrapConvert(Expression expression) + { + if (expression is UnaryExpression unaryExpression && + unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked) + { + return unaryExpression.Operand; + } + + return expression; + } + public static Expression PrepareExpression(Expression sourceExpression, IReadOnlyDictionary assignments) { var lambda = ConvertAssignmentsToBlockExpression(assignments); From 3d0c6aef3f97b0c29d72bc525db8704ce133dbaa Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 3 Nov 2025 10:42:25 +1000 Subject: [PATCH 4/4] Cleanup --- src/NHibernate/Linq/DmlExpressionRewriter.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/NHibernate/Linq/DmlExpressionRewriter.cs b/src/NHibernate/Linq/DmlExpressionRewriter.cs index 6f2b5df3c4d..57415affbc4 100644 --- a/src/NHibernate/Linq/DmlExpressionRewriter.cs +++ b/src/NHibernate/Linq/DmlExpressionRewriter.cs @@ -122,7 +122,7 @@ public static Expression PrepareExpressionFromAnonymous(Expression sour throw new ArgumentNullException(nameof(expression)); // Anonymous initializations are not implemented as member initialization but as plain constructor call, potentially wrapped in a Convert expression - var newExpression = UnwrapConvert(expression.Body) as NewExpression ?? + var newExpression = UnwrapConvertExpression(expression.Body) as NewExpression ?? throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }"); var instance = new DmlExpressionRewriter(expression.Parameters); @@ -130,12 +130,11 @@ public static Expression PrepareExpressionFromAnonymous(Expression sour return PrepareExpression(sourceExpression, instance._assignments); } - private static Expression UnwrapConvert(Expression expression) + private static Expression UnwrapConvertExpression(Expression expression) { - if (expression is UnaryExpression unaryExpression && - unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked) + if (expression is UnaryExpression ue && ue.NodeType == ExpressionType.Convert) { - return unaryExpression.Operand; + return ue.Operand; } return expression;