Skip to content

Commit da56a73

Browse files
bahusoidhazzik
authored andcommitted
Fix TransientObjectException in ISession.IsDirty() with many-to-one (#1420)
Add an explicit check for transient objects in ManyToOne.IsDirty Fixes #1419
1 parent 6bca2b0 commit da56a73

File tree

5 files changed

+328
-41
lines changed

5 files changed

+328
-41
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Linq;
13+
using NHibernate.Cfg.MappingSchema;
14+
using NHibernate.Mapping.ByCode;
15+
using NUnit.Framework;
16+
17+
namespace NHibernate.Test.NHSpecificTest.GH1419
18+
{
19+
using System.Threading.Tasks;
20+
using System.Threading;
21+
[TestFixture]
22+
public class ByCodeFixtureAsync : TestCaseMappingByCode
23+
{
24+
private Guid ParentId;
25+
26+
protected override HbmMapping GetMappings()
27+
{
28+
var mapper = new ModelMapper();
29+
mapper.Class<EntityParent>(rc =>
30+
{
31+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
32+
rc.Property(x => x.Name);
33+
rc.ManyToOne(ep => ep.Child);
34+
rc.ManyToOne(ep => ep.ChildAssigned);
35+
});
36+
37+
mapper.Class<EntityChild>(rc =>
38+
{
39+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
40+
rc.Property(x => x.Name);
41+
});
42+
43+
mapper.Class<EntityChildAssigned>(rc =>
44+
{
45+
rc.Id(x => x.Id, m => m.Generator(Generators.Assigned));
46+
rc.Property(x => x.Name);
47+
});
48+
49+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
50+
}
51+
52+
protected override void OnSetUp()
53+
{
54+
using (var session = OpenSession())
55+
using (var transaction = session.BeginTransaction())
56+
{
57+
var child = new EntityChild { Name = "InitialChild" };
58+
59+
var assigned = new EntityChildAssigned { Id = 1, Name = "InitialChild" };
60+
61+
var parent = new EntityParent
62+
{
63+
Name = "InitialParent",
64+
Child = child,
65+
ChildAssigned = assigned
66+
};
67+
session.Save(child);
68+
session.Save(parent);
69+
session.Save(assigned);
70+
71+
session.Flush();
72+
transaction.Commit();
73+
ParentId = parent.Id;
74+
}
75+
}
76+
77+
protected override void OnTearDown()
78+
{
79+
using (var session = OpenSession())
80+
using (var transaction = session.BeginTransaction())
81+
{
82+
session.Delete("from System.Object");
83+
84+
session.Flush();
85+
transaction.Commit();
86+
}
87+
}
88+
89+
[Test]
90+
public async Task SessionIsDirtyShouldNotFailForNewManyToOneObjectAsync()
91+
{
92+
using (var session = OpenSession())
93+
using (session.BeginTransaction())
94+
{
95+
var parent = await (GetParentAsync(session));
96+
97+
//parent.Child entity is not cascaded, I want to save it explictilty later
98+
parent.Child = new EntityChild { Name = "NewManyToOneChild" };
99+
100+
var isDirty = false;
101+
Assert.That(async () => isDirty = await (session.IsDirtyAsync()), Throws.Nothing, "ISession.IsDirty() call should not fail for transient many-to-one object referenced in session.");
102+
Assert.That(isDirty, "ISession.IsDirty() call should return true.");
103+
}
104+
}
105+
106+
[Test]
107+
public async Task SessionIsDirtyShouldNotFailForNewManyToOneObjectWithAssignedIdAsync()
108+
{
109+
using (var session = OpenSession())
110+
using (session.BeginTransaction())
111+
{
112+
var parent = await (GetParentAsync(session));
113+
114+
//parent.ChildAssigned entity is not cascaded, I want to save it explictilty later
115+
parent.ChildAssigned = new EntityChildAssigned { Id = 2, Name = "NewManyToOneChildAssignedId" };
116+
117+
var isDirty = false;
118+
Assert.That(async () => isDirty = await (session.IsDirtyAsync()), Throws.Nothing, "ISession.IsDirty() call should not fail for transient many-to-one object referenced in session.");
119+
Assert.That(isDirty, "ISession.IsDirty() call should return true.");
120+
}
121+
}
122+
123+
private Task<EntityParent> GetParentAsync(ISession session, CancellationToken cancellationToken = default(CancellationToken))
124+
{
125+
return session.GetAsync<EntityParent>(ParentId, cancellationToken);
126+
}
127+
}
128+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace NHibernate.Test.NHSpecificTest.GH1419
5+
{
6+
public class EntityChild
7+
{
8+
public virtual Guid Id { get; set; }
9+
public virtual string Name { get; set; }
10+
}
11+
12+
public class EntityParent
13+
{
14+
public virtual Guid Id { get; set; }
15+
public virtual string Name { get; set; }
16+
public virtual EntityChild Child { get; set; }
17+
public virtual EntityChildAssigned ChildAssigned { get; set; }
18+
}
19+
20+
public class EntityChildAssigned
21+
{
22+
public virtual int Id { get; set; }
23+
public virtual string Name { get; set; }
24+
}
25+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Linq;
3+
using NHibernate.Cfg.MappingSchema;
4+
using NHibernate.Mapping.ByCode;
5+
using NUnit.Framework;
6+
7+
namespace NHibernate.Test.NHSpecificTest.GH1419
8+
{
9+
[TestFixture]
10+
public class ByCodeFixture : TestCaseMappingByCode
11+
{
12+
private Guid ParentId;
13+
14+
protected override HbmMapping GetMappings()
15+
{
16+
var mapper = new ModelMapper();
17+
mapper.Class<EntityParent>(rc =>
18+
{
19+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
20+
rc.Property(x => x.Name);
21+
rc.ManyToOne(ep => ep.Child);
22+
rc.ManyToOne(ep => ep.ChildAssigned);
23+
});
24+
25+
mapper.Class<EntityChild>(rc =>
26+
{
27+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
28+
rc.Property(x => x.Name);
29+
});
30+
31+
mapper.Class<EntityChildAssigned>(rc =>
32+
{
33+
rc.Id(x => x.Id, m => m.Generator(Generators.Assigned));
34+
rc.Property(x => x.Name);
35+
});
36+
37+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
38+
}
39+
40+
protected override void OnSetUp()
41+
{
42+
using (var session = OpenSession())
43+
using (var transaction = session.BeginTransaction())
44+
{
45+
var child = new EntityChild { Name = "InitialChild" };
46+
47+
var assigned = new EntityChildAssigned { Id = 1, Name = "InitialChild" };
48+
49+
var parent = new EntityParent
50+
{
51+
Name = "InitialParent",
52+
Child = child,
53+
ChildAssigned = assigned
54+
};
55+
session.Save(child);
56+
session.Save(parent);
57+
session.Save(assigned);
58+
59+
session.Flush();
60+
transaction.Commit();
61+
ParentId = parent.Id;
62+
}
63+
}
64+
65+
protected override void OnTearDown()
66+
{
67+
using (var session = OpenSession())
68+
using (var transaction = session.BeginTransaction())
69+
{
70+
session.Delete("from System.Object");
71+
72+
session.Flush();
73+
transaction.Commit();
74+
}
75+
}
76+
77+
[Test]
78+
public void SessionIsDirtyShouldNotFailForNewManyToOneObject()
79+
{
80+
using (var session = OpenSession())
81+
using (session.BeginTransaction())
82+
{
83+
var parent = GetParent(session);
84+
85+
//parent.Child entity is not cascaded, I want to save it explictilty later
86+
parent.Child = new EntityChild { Name = "NewManyToOneChild" };
87+
88+
var isDirty = false;
89+
Assert.That(() => isDirty = session.IsDirty(), Throws.Nothing, "ISession.IsDirty() call should not fail for transient many-to-one object referenced in session.");
90+
Assert.That(isDirty, "ISession.IsDirty() call should return true.");
91+
}
92+
}
93+
94+
[Test]
95+
public void SessionIsDirtyShouldNotFailForNewManyToOneObjectWithAssignedId()
96+
{
97+
using (var session = OpenSession())
98+
using (session.BeginTransaction())
99+
{
100+
var parent = GetParent(session);
101+
102+
//parent.ChildAssigned entity is not cascaded, I want to save it explictilty later
103+
parent.ChildAssigned = new EntityChildAssigned { Id = 2, Name = "NewManyToOneChildAssignedId" };
104+
105+
var isDirty = false;
106+
Assert.That(() => isDirty = session.IsDirty(), Throws.Nothing, "ISession.IsDirty() call should not fail for transient many-to-one object referenced in session.");
107+
Assert.That(isDirty, "ISession.IsDirty() call should return true.");
108+
}
109+
}
110+
111+
private EntityParent GetParent(ISession session)
112+
{
113+
return session.Get<EntityParent>(ParentId);
114+
}
115+
}
116+
}

src/NHibernate/Async/Type/ManyToOneType.cs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,37 +135,49 @@ private Task<object> AssembleIdAsync(object oid, ISessionImplementor session, Ca
135135
}
136136
}
137137

138-
public override async Task<bool> IsDirtyAsync(object old, object current, ISessionImplementor session, CancellationToken cancellationToken)
138+
public override Task<bool> IsDirtyAsync(object old, object current, ISessionImplementor session, CancellationToken cancellationToken)
139139
{
140-
cancellationToken.ThrowIfCancellationRequested();
141-
if (IsSame(old, current))
140+
if (cancellationToken.IsCancellationRequested)
142141
{
143-
return false;
142+
return Task.FromCanceled<bool>(cancellationToken);
144143
}
144+
return IsDirtyManyToOneAsync(old, current, null, session, cancellationToken);
145+
}
145146

146-
object oldid = await (GetIdentifierAsync(old, session, cancellationToken)).ConfigureAwait(false);
147-
object newid = await (GetIdentifierAsync(current, session, cancellationToken)).ConfigureAwait(false);
148-
return await (GetIdentifierType(session).IsDirtyAsync(oldid, newid, session, cancellationToken)).ConfigureAwait(false);
147+
public override Task<bool> IsDirtyAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken)
148+
{
149+
if (cancellationToken.IsCancellationRequested)
150+
{
151+
return Task.FromCanceled<bool>(cancellationToken);
152+
}
153+
return IsDirtyManyToOneAsync(old, current, IsAlwaysDirtyChecked ? null : checkable, session, cancellationToken);
149154
}
150155

151-
public override async Task<bool> IsDirtyAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken)
156+
private async Task<bool> IsDirtyManyToOneAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken)
152157
{
153158
cancellationToken.ThrowIfCancellationRequested();
154-
if (IsAlwaysDirtyChecked)
159+
if (IsSame(old, current))
155160
{
156-
return await (IsDirtyAsync(old, current, session, cancellationToken)).ConfigureAwait(false);
161+
return false;
157162
}
158-
else
163+
164+
if (old == null || current == null)
159165
{
160-
if (IsSame(old, current))
161-
{
162-
return false;
163-
}
166+
return true;
167+
}
164168

165-
object oldid = await (GetIdentifierAsync(old, session, cancellationToken)).ConfigureAwait(false);
166-
object newid = await (GetIdentifierAsync(current, session, cancellationToken)).ConfigureAwait(false);
167-
return await (GetIdentifierType(session).IsDirtyAsync(oldid, newid, checkable, session, cancellationToken)).ConfigureAwait(false);
169+
if ((await (ForeignKeys.IsTransientFastAsync(GetAssociatedEntityName(), current, session, cancellationToken)).ConfigureAwait(false)).GetValueOrDefault())
170+
{
171+
return true;
168172
}
173+
174+
object oldid = await (GetIdentifierAsync(old, session, cancellationToken)).ConfigureAwait(false);
175+
object newid = await (GetIdentifierAsync(current, session, cancellationToken)).ConfigureAwait(false);
176+
IType identifierType = GetIdentifierType(session);
177+
178+
return checkable == null
179+
? await (identifierType.IsDirtyAsync(oldid, newid, session, cancellationToken)).ConfigureAwait(false)
180+
: await (identifierType.IsDirtyAsync(oldid, newid, checkable, session, cancellationToken)).ConfigureAwait(false);
169181
}
170182
}
171183
}

0 commit comments

Comments
 (0)