Skip to content

Commit 63d659e

Browse files
NH-3488 - Anonymous selector for Linq insert/update
1 parent 6aa9c18 commit 63d659e

File tree

4 files changed

+188
-43
lines changed

4 files changed

+188
-43
lines changed

src/NHibernate.Test/LinqBulkManipulation/Fixture.cs

Lines changed: 105 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,20 @@ public void SimpleInsert()
187187
using (var s = OpenSession())
188188
using (var t = s.BeginTransaction())
189189
{
190-
var count = s.Query<Car>().Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner });
190+
var count = s.Query<Car>().Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
191+
Assert.AreEqual(1, count);
192+
193+
t.Commit();
194+
}
195+
}
196+
197+
[Test]
198+
public void SimpleAnonymousInsert()
199+
{
200+
using (var s = OpenSession())
201+
using (var t = s.BeginTransaction())
202+
{
203+
var count = s.Query<Car>().Insert().As<Pickup>(x => new { Id = -x.Id, x.Vin, x.Owner });
191204
Assert.AreEqual(1, count);
192205

193206
t.Commit();
@@ -200,10 +213,11 @@ public void SimpleInsertFromAggregate()
200213
using (var s = OpenSession())
201214
using (var t = s.BeginTransaction())
202215
{
203-
var count = s.Query<Car>()
216+
var count = s
217+
.Query<Car>()
204218
.GroupBy(x => x.Id)
205219
.Select(x => new { Id = x.Key, Vin = x.Max(y => y.Vin), Owner = x.Max(y => y.Owner) })
206-
.Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner });
220+
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
207221
Assert.AreEqual(1, count);
208222

209223
t.Commit();
@@ -216,7 +230,8 @@ public void SimpleInsertFromLimited()
216230
using (var s = OpenSession())
217231
using (var t = s.BeginTransaction())
218232
{
219-
var count = s.Query<Vehicle>()
233+
var count = s
234+
.Query<Vehicle>()
220235
.Skip(1)
221236
.Take(1)
222237
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner });
@@ -232,8 +247,9 @@ public void SimpleInsertWithConstants()
232247
using (var s = OpenSession())
233248
using (var t = s.BeginTransaction())
234249
{
235-
var count = s.Query<Car>()
236-
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner"));
250+
var count = s
251+
.Query<Car>()
252+
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.Vin).Set(y => y.Owner, "The owner"));
237253
Assert.AreEqual(1, count);
238254

239255
t.Commit();
@@ -246,9 +262,10 @@ public void SimpleInsertFromProjection()
246262
using (var s = OpenSession())
247263
using (var t = s.BeginTransaction())
248264
{
249-
var count = s.Query<Car>()
265+
var count = s
266+
.Query<Car>()
250267
.Select(x => new { x.Id, x.Owner, UpperOwner = x.Owner.ToUpper() })
251-
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => y.Id).Set(y => y.Vin, y => y.UpperOwner));
268+
.Insert().Into<Pickup>(x => x.Set(y => y.Id, y => -y.Id).Set(y => y.Vin, y => y.UpperOwner));
252269
Assert.AreEqual(1, count);
253270

254271
t.Commit();
@@ -261,9 +278,10 @@ public void InsertWithClientSideRequirementsThrowsException()
261278
using (var s = OpenSession())
262279
using (var t = s.BeginTransaction())
263280
{
264-
Assert.Throws<NotSupportedException>(() =>
265-
s.Query<Car>()
266-
.Insert().As(x => new Pickup { Id = x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) }));
281+
Assert.Throws<NotSupportedException>(
282+
() => s
283+
.Query<Car>()
284+
.Insert().As(x => new Pickup { Id = -x.Id, Vin = x.Vin, Owner = x.Owner.PadRight(200) }));
267285

268286
t.Commit();
269287
}
@@ -277,7 +295,8 @@ public void InsertWithManyToOne()
277295
using (var s = OpenSession())
278296
using (var t = s.BeginTransaction())
279297
{
280-
var count = s.Query<Human>()
298+
var count = s
299+
.Query<Human>()
281300
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = x.Mother });
282301
Assert.AreEqual(3, count);
283302

@@ -293,7 +312,8 @@ public void InsertWithManyToOneAsParameter()
293312
using (var s = OpenSession())
294313
using (var t = s.BeginTransaction())
295314
{
296-
var count = s.Query<Human>()
315+
var count = s
316+
.Query<Human>()
297317
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight, Mother = _butterfly });
298318
Assert.AreEqual(3, count);
299319

@@ -309,7 +329,8 @@ public void InsertWithManyToOneWithCompositeKey()
309329
using (var s = OpenSession())
310330
using (var t = s.BeginTransaction())
311331
{
312-
var count = s.Query<EntityWithCrazyCompositeKey>()
332+
var count = s
333+
.Query<EntityWithCrazyCompositeKey>()
313334
.Insert().As(x => new EntityReferencingEntityWithCrazyCompositeKey { Name = "Child", Parent = x });
314335
Assert.AreEqual(1, count);
315336

@@ -324,7 +345,7 @@ public void InsertIntoSuperclassPropertiesFails()
324345
using (var t = s.BeginTransaction())
325346
{
326347
Assert.Throws<QueryException>(
327-
() => s.Query<Lizard>().Insert().As(x => new Human { Id = x.Id, BodyWeight = x.BodyWeight }),
348+
() => s.Query<Lizard>().Insert().As(x => new Human { Id = -x.Id, BodyWeight = x.BodyWeight }),
328349
"superclass prop insertion did not error");
329350

330351
t.Commit();
@@ -381,10 +402,10 @@ public void InsertWithGeneratedVersionAndId()
381402
using (var s = OpenSession())
382403
using (var t = s.BeginTransaction())
383404
{
384-
var count =
385-
s.Query<IntegerVersioned>()
386-
.Where(x => x.Id == initialId)
387-
.Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data });
405+
var count = s
406+
.Query<IntegerVersioned>()
407+
.Where(x => x.Id == initialId)
408+
.Insert().As(x => new IntegerVersioned { Name = x.Name, Data = x.Data });
388409
Assert.That(count, Is.EqualTo(1), "unexpected insertion count");
389410
t.Commit();
390411
}
@@ -408,10 +429,10 @@ public void InsertWithGeneratedTimestampVersion()
408429
using (var s = OpenSession())
409430
using (var t = s.BeginTransaction())
410431
{
411-
var count =
412-
s.Query<TimestampVersioned>()
413-
.Where(x => x.Id == initialId)
414-
.Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data });
432+
var count = s
433+
.Query<TimestampVersioned>()
434+
.Where(x => x.Id == initialId)
435+
.Insert().As(x => new TimestampVersioned { Name = x.Name, Data = x.Data });
415436
Assert.That(count, Is.EqualTo(1), "unexpected insertion count");
416437

417438
t.Commit();
@@ -438,7 +459,8 @@ public void InsertWithSelectListUsingJoins()
438459

439460
Assert.DoesNotThrow(() =>
440461
{
441-
s.Query<Human>().Where(x => x.Mother.Mother != null)
462+
s
463+
.Query<Human>().Where(x => x.Mother.Mother != null)
442464
.Insert().As(x => new Animal { Description = x.Description, BodyWeight = x.BodyWeight });
443465
});
444466

@@ -462,13 +484,25 @@ public void InsertToComponent()
462484
// https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-insert.html#fblangref25-dml-insert-select-unstable
463485
.Where(sc => sc.Name.First != correctName)
464486
.Insert().Into<SimpleClassWithComponent>(x => x.Set(y => y.Name.First, y => correctName));
465-
Assert.That(count, Is.EqualTo(1), "incorrect insert count");
487+
Assert.That(count, Is.EqualTo(1), "incorrect insert count from individual setters");
466488

467-
count =
468-
s.Query<SimpleClassWithComponent>()
469-
.Where(x => x.Name.First == correctName && x.Name.Initial != 'Z')
470-
.Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } });
471-
Assert.That(count, Is.EqualTo(1), "incorrect insert from corrected count");
489+
count = s
490+
.Query<SimpleClassWithComponent>()
491+
.Where(x => x.Name.First == correctName && x.Name.Initial != 'Z')
492+
.Insert().As(x => new SimpleClassWithComponent { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'Z' } });
493+
Assert.That(count, Is.EqualTo(1), "incorrect insert from non anonymous selector");
494+
495+
count = s
496+
.Query<SimpleClassWithComponent>()
497+
.Where(x => x.Name.First == correctName && x.Name.Initial == 'Z')
498+
.Insert().As<SimpleClassWithComponent>(x => new { Name = new { x.Name.First, x.Name.Last, Initial = 'W' } });
499+
Assert.That(count, Is.EqualTo(1), "incorrect insert from anonymous selector");
500+
501+
count = s
502+
.Query<SimpleClassWithComponent>()
503+
.Where(x => x.Name.First == correctName && x.Name.Initial == 'Z')
504+
.Insert().As<SimpleClassWithComponent>(x => new { Name = new Name { First = x.Name.First, Last = x.Name.Last, Initial = 'V' } });
505+
Assert.That(count, Is.EqualTo(1), "incorrect insert from hybrid selector");
472506
t.Commit();
473507
}
474508
}
@@ -488,6 +522,34 @@ private void CheckSupportOfBulkInsertionWithGeneratedId<T>()
488522

489523
#region UPDATES
490524

525+
[Test]
526+
public void SimpleUpdate()
527+
{
528+
using (var s = OpenSession())
529+
using (s.BeginTransaction())
530+
{
531+
var count = s
532+
.Query<Car>()
533+
.Update()
534+
.As(a => new Car { Owner = a.Owner + " a" });
535+
Assert.AreEqual(1, count);
536+
}
537+
}
538+
539+
[Test]
540+
public void SimpleAnonymousUpdate()
541+
{
542+
using (var s = OpenSession())
543+
using (s.BeginTransaction())
544+
{
545+
var count = s
546+
.Query<Car>()
547+
.Update()
548+
.As(a => new { Owner = a.Owner + " a" });
549+
Assert.AreEqual(1, count);
550+
}
551+
}
552+
491553
[Test]
492554
public void UpdateWithWhereExistsSubquery()
493555
{
@@ -501,7 +563,8 @@ public void UpdateWithWhereExistsSubquery()
501563
using (var s = OpenSession())
502564
using (var t = s.BeginTransaction())
503565
{
504-
var count = s.Query<Human>()
566+
var count = s
567+
.Query<Human>()
505568
.Where(x => x.Friends.OfType<Human>().Any(f => f.Name.Last == "Public"))
506569
.Update().Assign(x => x.Set(y => y.Description, "updated"));
507570
Assert.That(count, Is.EqualTo(1));
@@ -513,14 +576,16 @@ public void UpdateWithWhereExistsSubquery()
513576
using (var t = s.BeginTransaction())
514577
{
515578
// one-to-many test
516-
var count = s.Query<SimpleEntityWithAssociation>()
579+
var count = s
580+
.Query<SimpleEntityWithAssociation>()
517581
.Where(x => x.AssociatedEntities.Any(a => a.Name == "one-to-many-association"))
518582
.Update().Assign(x => x.Set(y => y.Name, "updated"));
519583
Assert.That(count, Is.EqualTo(1));
520584
// many-to-many test
521585
if (Dialect.SupportsSubqueryOnMutatingTable)
522586
{
523-
count = s.Query<SimpleEntityWithAssociation>()
587+
count = s
588+
.Query<SimpleEntityWithAssociation>()
524589
.Where(x => x.ManyToManyAssociatedEntities.Any(a => a.Name == "many-to-many-association"))
525590
.Update().Assign(x => x.Set(y => y.Name, "updated"));
526591

@@ -540,9 +605,9 @@ public void IncrementCounterVersion()
540605
using (var t = s.BeginTransaction())
541606
{
542607
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
543-
var count =
544-
s.Query<IntegerVersioned>()
545-
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
608+
var count = s
609+
.Query<IntegerVersioned>()
610+
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
546611
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
547612
t.Commit();
548613
}
@@ -570,7 +635,8 @@ public void IncrementTimestampVersion()
570635
using (var t = s.BeginTransaction())
571636
{
572637
// Note: Update more than one column to showcase NH-3624, which involved losing some columns. /2014-07-26
573-
var count = s.Query<TimestampVersioned>()
638+
var count = s
639+
.Query<TimestampVersioned>()
574640
.UpdateVersioned().Assign(x => x.Set(y => y.Name, y => y.Name + "upd").Set(y => y.Data, y => y.Data + "upd"));
575641
Assert.That(count, Is.EqualTo(1), "incorrect exec count");
576642
t.Commit();
@@ -625,8 +691,8 @@ public void UpdateWithClientSideRequirementsThrowsException()
625691
using (var s = OpenSession())
626692
using (var t = s.BeginTransaction())
627693
{
628-
Assert.Throws<NotSupportedException>(() =>
629-
s.Query<Human>().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } })
694+
Assert.Throws<NotSupportedException>(
695+
() => s.Query<Human>().Where(x => x.Id == _stevee.Id).Update().As(x => new Human { Name = { First = x.Name.First.PadLeft(200) } })
630696
);
631697

632698
t.Commit();

src/NHibernate/Linq/DmlExpressionRewriter.cs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,49 @@ public class DmlExpressionRewriter
2525
void AddSettersFromBindings(IEnumerable<MemberBinding> bindings, string path)
2626
{
2727
foreach (var node in bindings)
28+
{
29+
var subPath = path + "." + node.Member.Name;
2830
switch (node.BindingType)
2931
{
3032
case MemberBindingType.Assignment:
31-
AddSettersFromAssignment((MemberAssignment) node, path + "." + node.Member.Name);
33+
AddSettersFromAssignment((MemberAssignment)node, subPath);
3234
break;
3335
case MemberBindingType.MemberBinding:
34-
AddSettersFromBindings(((MemberMemberBinding) node).Bindings, path + "." + node.Member.Name);
36+
AddSettersFromBindings(((MemberMemberBinding)node).Bindings, subPath);
3537
break;
3638
default:
3739
throw new InvalidOperationException($"{node.BindingType} is not supported");
3840
}
41+
}
42+
}
43+
44+
void AddSettersFromConstructorArguments(NewExpression newExpression, string path)
45+
{
46+
// See Members documentation, this property is specifically designed to match constructor arguments values
47+
// in the anonymous object case. It can be null otherwise, or non-matching.
48+
var argumentMatchingMembers = newExpression.Members;
49+
if (argumentMatchingMembers == null || argumentMatchingMembers.Count != newExpression.Arguments.Count)
50+
throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }");
51+
52+
var i = 0;
53+
foreach (var argument in newExpression.Arguments)
54+
{
55+
var argumentDefinition = argumentMatchingMembers[i];
56+
i++;
57+
var subPath = path + "." + argumentDefinition.Name;
58+
switch (argument.NodeType)
59+
{
60+
case ExpressionType.New:
61+
AddSettersFromConstructorArguments((NewExpression)argument, subPath);
62+
break;
63+
case ExpressionType.MemberInit:
64+
AddSettersFromBindings(((MemberInitExpression)argument).Bindings, subPath);
65+
break;
66+
default:
67+
_assignments.Add(subPath.Substring(1), Expression.Lambda(argument, _parameters));
68+
break;
69+
}
70+
}
3971
}
4072

4173
void AddSettersFromAssignment(MemberAssignment assignment, string path)
@@ -89,12 +121,28 @@ public static Expression PrepareExpression<TSource, TTarget>(Expression sourceEx
89121
throw new ArgumentNullException(nameof(expression));
90122

91123
var memberInitExpression = expression.Body as MemberInitExpression ??
92-
throw new ArgumentException("The expression must be member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }");
124+
throw new ArgumentException("The expression must be a member initialization, e.g. x => new Dog { Name = x.Name, Age = x.Age + 5 }, " +
125+
// If someone call InsertSyntax<TSource>.As(source => new {...}), the code will fail here, so we have to hint at how to correctly
126+
// use anonymous initialization too.
127+
"or an anonymous initialization with an explicitly specified target type when inserting");
93128

94129
var assignments = ExtractAssignments(expression, memberInitExpression);
95130
return PrepareExpression<TSource>(sourceExpression, assignments);
96131
}
97132

133+
public static Expression PrepareExpression<TSource>(Expression sourceExpression, Expression<Func<TSource, object>> expression)
134+
{
135+
if (expression == null)
136+
throw new ArgumentNullException(nameof(expression));
137+
138+
// Anonymous initializations are not implemented as member initialization but as plain constructor call.
139+
var newExpression = expression.Body as NewExpression ??
140+
throw new ArgumentException("The expression must be an anonymous initialization, e.g. x => new { Name = x.Name, Age = x.Age + 5 }");
141+
142+
var assignments = ExtractAssignments(expression, newExpression);
143+
return PrepareExpression<TSource>(sourceExpression, assignments);
144+
}
145+
98146
public static Expression PrepareExpression<TSource>(Expression sourceExpression, IReadOnlyDictionary<string, Expression> assignments)
99147
{
100148
var lambda = ConvertAssignmentsToDictionaryExpression<TSource>(assignments);
@@ -108,10 +156,18 @@ public static Expression PrepareExpression<TSource>(Expression sourceExpression,
108156
static Dictionary<string, Expression> ExtractAssignments<TSource, TTarget>(Expression<Func<TSource, TTarget>> expression, MemberInitExpression memberInitExpression)
109157
{
110158
if (memberInitExpression.Type != typeof(TTarget))
111-
throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} but got {memberInitExpression.Type.AssemblyQualifiedName}");
159+
throw new TypeMismatchException($"Expecting an expression of exact type {typeof(TTarget).AssemblyQualifiedName} " +
160+
$"but got {memberInitExpression.Type.AssemblyQualifiedName}");
112161
var instance = new DmlExpressionRewriter(expression.Parameters);
113162
instance.AddSettersFromBindings(memberInitExpression.Bindings, "");
114163
return instance._assignments;
115164
}
165+
166+
static Dictionary<string, Expression> ExtractAssignments<TSource>(Expression<Func<TSource, object>> expression, NewExpression newExpression)
167+
{
168+
var instance = new DmlExpressionRewriter(expression.Parameters);
169+
instance.AddSettersFromConstructorArguments(newExpression, "");
170+
return instance._assignments;
171+
}
116172
}
117173
}

0 commit comments

Comments
 (0)