diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2552/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2552/Fixture.cs new file mode 100644 index 00000000000..ead8cfc3788 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2552/Fixture.cs @@ -0,0 +1,224 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Stat; +using NUnit.Framework; +using NHCfg = NHibernate.Cfg; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.GH2552 +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override string CacheConcurrencyStrategy => null; + + protected override void Configure(NHCfg.Configuration configuration) + { + configuration.SetProperty(NHCfg.Environment.UseSecondLevelCache, "true"); + configuration.SetProperty(NHCfg.Environment.GenerateStatistics, "true"); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from DetailsByFK").ExecuteUpdate(); + s.CreateQuery("delete from PersonByFK").ExecuteUpdate(); + s.CreateQuery("delete from DetailsByRef").ExecuteUpdate(); + s.CreateQuery("delete from PersonByRef").ExecuteUpdate(); + + tx.Commit(); + } + + Sfi.Evict(typeof(PersonByFK)); + Sfi.Evict(typeof(DetailsByFK)); + Sfi.Evict(typeof(PersonByRef)); + Sfi.Evict(typeof(DetailsByRef)); + } + + private async Task OneToOneFetchTestAsync(CancellationToken cancellationToken = default(CancellationToken)) where TPerson : Person, new() where TDetails : Details, new() + { + List ids = await (this.CreatePersonAndDetailsAsync(cancellationToken)); + + IStatistics statistics = Sfi.Statistics; + + // Clear the second level cache and the statistics + await (Sfi.EvictEntityAsync(typeof(TPerson).FullName, cancellationToken)); + await (Sfi.EvictEntityAsync(typeof(TDetails).FullName, cancellationToken)); + await (Sfi.EvictQueriesAsync(cancellationToken)); + + statistics.Clear(); + + // Fill the empty caches with data. + await (this.FetchPeopleByIdAsync(ids, cancellationToken)); + + // Verify that no data was retrieved from the cache. + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + + statistics.Clear(); + + await (this.FetchPeopleByIdAsync(ids, cancellationToken)); + + Assert.AreEqual(0, statistics.SecondLevelCacheMissCount, "Second level cache miss count"); + } + + private async Task OneToOneUpdateTestAsync(CancellationToken cancellationToken = default(CancellationToken)) where TPerson : Person, new() where TDetails : Details, new() + { + List ids = await (this.CreatePersonAndDetailsAsync(cancellationToken)); + + IStatistics statistics = Sfi.Statistics; + + // Clear the second level cache and the statistics + await (Sfi.EvictEntityAsync(typeof(TPerson).FullName, cancellationToken)); + await (Sfi.EvictEntityAsync(typeof(TDetails).FullName, cancellationToken)); + await (Sfi.EvictQueriesAsync(cancellationToken)); + + statistics.Clear(); + + // Fill the empty caches with data. + await (this.FetchPeopleByIdAsync(ids, cancellationToken)); + + // Verify that no data was retrieved from the cache. + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + statistics.Clear(); + + int personId = await (DeleteDetailsFromFirstPersonAsync(cancellationToken)); + + // Verify that the cache was updated + Assert.AreEqual(1, statistics.SecondLevelCachePutCount, "Second level cache put count"); + statistics.Clear(); + + // Verify that the Person was updated in the cache + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + TPerson person = await (s.GetAsync(personId, cancellationToken)); + + Assert.IsNull(person.Details); + } + + Assert.AreEqual(0, statistics.SecondLevelCacheMissCount, "Second level cache miss count"); + statistics.Clear(); + + // Verify that the Details was removed from the cache and deleted. + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + TDetails details = await (s.GetAsync(personId, cancellationToken)); + + Assert.Null(details); + } + + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + } + + private async Task DeleteDetailsFromFirstPersonAsync(CancellationToken cancellationToken = default(CancellationToken)) where TPerson:Person + { + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + // Get the first person with details. + Person person = await (s.Query() + .Where(p => p.Details != null) + .Take(1) + .SingleOrDefaultAsync(cancellationToken)); + + Assert.NotNull(person); + Assert.NotNull(person.Details); + + person.Details = null; + + await (tx.CommitAsync(cancellationToken)); + + return person.Id; + } + } + + private async Task> CreatePersonAndDetailsAsync(CancellationToken cancellationToken = default(CancellationToken)) where TPerson : Person, new() where TDetails : Details, new() + { + List ids = new List(); + + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + for (int i = 0; i < 6; i++) + { + Person person = new TPerson(); + + if (i % 2 == 0) + { + Details details = new TDetails(); + + details.Data = String.Format("{0}{1}", typeof(TDetails).Name, i); + + person.Details = details; + } + + person.Name = String.Format("{0}{1}", typeof(TPerson).Name, i); + + ids.Add(await (s.SaveAsync(person, cancellationToken))); + } + + await (tx.CommitAsync(cancellationToken)); + } + + return ids; + } + + public async Task> FetchPeopleByIdAsync(List ids, CancellationToken cancellationToken = default(CancellationToken)) where TPerson : Person + { + IList people = new List(); + + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + foreach (object id in ids) + { + people.Add(await (s.GetAsync(id, cancellationToken))); + } + + await (tx.CommitAsync(cancellationToken)); + } + + return people; + } + + [Test] + public async Task OneToOneCacheFetchByForeignKeyAsync() + { + await (OneToOneFetchTestAsync()); + } + + [Test] + public async Task OneToOneCacheFetchByRefAsync() + { + await (OneToOneFetchTestAsync()); + } + + [Test] + public async Task OneToOneCacheUpdateByForeignKeyAsync() + { + await (OneToOneUpdateTestAsync()); + } + + [Test] + public async Task OneToOneCacheUpdateByRefAsync() + { + await (OneToOneUpdateTestAsync()); + } + } +} diff --git a/src/NHibernate.Test/Async/OneToOneType/Fixture.cs b/src/NHibernate.Test/Async/OneToOneType/Fixture.cs new file mode 100644 index 00000000000..17c8d4ae697 --- /dev/null +++ b/src/NHibernate.Test/Async/OneToOneType/Fixture.cs @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Test.NHSpecificTest; +using NUnit.Framework; + +namespace NHibernate.Test.OneToOneType +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override void OnTearDown() + { + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Details").ExecuteUpdate(); + s.CreateQuery("delete from Owner").ExecuteUpdate(); + + tx.Commit(); + } + } + + [Test] + public async Task OneToOnePersistedOnOwnerUpdateAsync() + { + object ownerId; + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + var owner = new Owner() + { + Name = "Owner", + }; + + ownerId = await (s.SaveAsync(owner)); + + await (tx.CommitAsync()); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + Owner owner = await (s.LoadAsync(ownerId)); + + owner.Details = new Details() + { + Data = "Owner Details" + }; + + await (tx.CommitAsync()); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + Owner owner = await (s.GetAsync(ownerId)); + + Assert.NotNull(owner.Details); + } + } + + [Test] + public async Task OneToOnePersistedOnOwnerUpdateForSessionUpdateAsync() + { + Owner owner; + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + owner = new Owner() + { + Name = "Owner", + }; + + await (s.SaveAsync(owner)); + await (tx.CommitAsync()); + } + + using (var s = Sfi.OpenSession()) + { + owner = await (s.GetAsync(owner.Id)); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + await (s.SaveOrUpdateAsync(owner)); + owner.Details = new Details() + { + Data = "Owner Details" + }; + + await (tx.CommitAsync()); + } + + using (var s = Sfi.OpenSession()) + { + owner = await (s.GetAsync(owner.Id)); + + Assert.IsNotNull(owner.Details); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2552/Details.cs b/src/NHibernate.Test/NHSpecificTest/GH2552/Details.cs new file mode 100644 index 00000000000..103cfa1db00 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2552/Details.cs @@ -0,0 +1,11 @@ +namespace NHibernate.Test.NHSpecificTest.GH2552 +{ + public abstract class Details + { + public virtual int Id { get; protected set; } + public virtual Person Person { get; set; } + public virtual string Data { get; set; } + } + public class DetailsByFK : Details { } + public class DetailsByRef : Details { } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2552/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH2552/Fixture.cs new file mode 100644 index 00000000000..577d0ce3a8f --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2552/Fixture.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Stat; +using NUnit.Framework; +using NHCfg = NHibernate.Cfg; + +namespace NHibernate.Test.NHSpecificTest.GH2552 +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override string CacheConcurrencyStrategy => null; + + protected override void Configure(NHCfg.Configuration configuration) + { + configuration.SetProperty(NHCfg.Environment.UseSecondLevelCache, "true"); + configuration.SetProperty(NHCfg.Environment.GenerateStatistics, "true"); + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from DetailsByFK").ExecuteUpdate(); + s.CreateQuery("delete from PersonByFK").ExecuteUpdate(); + s.CreateQuery("delete from DetailsByRef").ExecuteUpdate(); + s.CreateQuery("delete from PersonByRef").ExecuteUpdate(); + + tx.Commit(); + } + + Sfi.Evict(typeof(PersonByFK)); + Sfi.Evict(typeof(DetailsByFK)); + Sfi.Evict(typeof(PersonByRef)); + Sfi.Evict(typeof(DetailsByRef)); + } + + private void OneToOneFetchTest() where TPerson : Person, new() where TDetails : Details, new() + { + List ids = this.CreatePersonAndDetails(); + + IStatistics statistics = Sfi.Statistics; + + // Clear the second level cache and the statistics + Sfi.EvictEntity(typeof(TPerson).FullName); + Sfi.EvictEntity(typeof(TDetails).FullName); + Sfi.EvictQueries(); + + statistics.Clear(); + + // Fill the empty caches with data. + this.FetchPeopleById(ids); + + // Verify that no data was retrieved from the cache. + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + + statistics.Clear(); + + this.FetchPeopleById(ids); + + Assert.AreEqual(0, statistics.SecondLevelCacheMissCount, "Second level cache miss count"); + } + + private void OneToOneUpdateTest() where TPerson : Person, new() where TDetails : Details, new() + { + List ids = this.CreatePersonAndDetails(); + + IStatistics statistics = Sfi.Statistics; + + // Clear the second level cache and the statistics + Sfi.EvictEntity(typeof(TPerson).FullName); + Sfi.EvictEntity(typeof(TDetails).FullName); + Sfi.EvictQueries(); + + statistics.Clear(); + + // Fill the empty caches with data. + this.FetchPeopleById(ids); + + // Verify that no data was retrieved from the cache. + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + statistics.Clear(); + + int personId = DeleteDetailsFromFirstPerson(); + + // Verify that the cache was updated + Assert.AreEqual(1, statistics.SecondLevelCachePutCount, "Second level cache put count"); + statistics.Clear(); + + // Verify that the Person was updated in the cache + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + TPerson person = s.Get(personId); + + Assert.IsNull(person.Details); + } + + Assert.AreEqual(0, statistics.SecondLevelCacheMissCount, "Second level cache miss count"); + statistics.Clear(); + + // Verify that the Details was removed from the cache and deleted. + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + TDetails details = s.Get(personId); + + Assert.Null(details); + } + + Assert.AreEqual(0, statistics.SecondLevelCacheHitCount, "Second level cache hit count"); + } + + private int DeleteDetailsFromFirstPerson() where TPerson:Person + { + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + // Get the first person with details. + Person person = s.Query() + .Where(p => p.Details != null) + .Take(1) + .SingleOrDefault(); + + Assert.NotNull(person); + Assert.NotNull(person.Details); + + person.Details = null; + + tx.Commit(); + + return person.Id; + } + } + + private List CreatePersonAndDetails() where TPerson : Person, new() where TDetails : Details, new() + { + List ids = new List(); + + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + for (int i = 0; i < 6; i++) + { + Person person = new TPerson(); + + if (i % 2 == 0) + { + Details details = new TDetails(); + + details.Data = String.Format("{0}{1}", typeof(TDetails).Name, i); + + person.Details = details; + } + + person.Name = String.Format("{0}{1}", typeof(TPerson).Name, i); + + ids.Add(s.Save(person)); + } + + tx.Commit(); + } + + return ids; + } + + public IList FetchPeopleById(List ids) where TPerson : Person + { + IList people = new List(); + + using (ISession s = Sfi.OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + foreach (object id in ids) + { + people.Add(s.Get(id)); + } + + tx.Commit(); + } + + return people; + } + + [Test] + public void OneToOneCacheFetchByForeignKey() + { + OneToOneFetchTest(); + } + + [Test] + public void OneToOneCacheFetchByRef() + { + OneToOneFetchTest(); + } + + [Test] + public void OneToOneCacheUpdateByForeignKey() + { + OneToOneUpdateTest(); + } + + [Test] + public void OneToOneCacheUpdateByRef() + { + OneToOneUpdateTest(); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2552/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH2552/Mappings.hbm.xml new file mode 100644 index 00000000000..0fd9f39b0e8 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2552/Mappings.hbm.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + Person + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/GH2552/Person.cs b/src/NHibernate.Test/NHSpecificTest/GH2552/Person.cs new file mode 100644 index 00000000000..c3d7691b7f9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2552/Person.cs @@ -0,0 +1,28 @@ +namespace NHibernate.Test.NHSpecificTest.GH2552 +{ + public abstract class Person + { + private Details _details; + + public virtual int Id { get; protected set; } + public virtual string Name { get; set; } + + public virtual Details Details + { + get { return _details; } + set + { + _details = value; + + if (_details != null) + { + _details.Person = this; + } + } + } + } + + public class PersonByFK : Person { } + + public class PersonByRef : Person { } +} diff --git a/src/NHibernate.Test/OneToOneType/Details.cs b/src/NHibernate.Test/OneToOneType/Details.cs new file mode 100644 index 00000000000..0ffb4fc799e --- /dev/null +++ b/src/NHibernate.Test/OneToOneType/Details.cs @@ -0,0 +1,9 @@ +namespace NHibernate.Test.OneToOneType +{ + public class Details + { + public virtual int Id { get; protected set; } + public virtual Owner Owner { get; protected internal set; } + public virtual string Data { get; set; } + } +} diff --git a/src/NHibernate.Test/OneToOneType/Fixture.cs b/src/NHibernate.Test/OneToOneType/Fixture.cs new file mode 100644 index 00000000000..7bfffee851f --- /dev/null +++ b/src/NHibernate.Test/OneToOneType/Fixture.cs @@ -0,0 +1,103 @@ +using NHibernate.Test.NHSpecificTest; +using NUnit.Framework; + +namespace NHibernate.Test.OneToOneType +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override void OnTearDown() + { + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.CreateQuery("delete from Details").ExecuteUpdate(); + s.CreateQuery("delete from Owner").ExecuteUpdate(); + + tx.Commit(); + } + } + + [Test] + public void OneToOnePersistedOnOwnerUpdate() + { + object ownerId; + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + var owner = new Owner() + { + Name = "Owner", + }; + + ownerId = s.Save(owner); + + tx.Commit(); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + Owner owner = s.Load(ownerId); + + owner.Details = new Details() + { + Data = "Owner Details" + }; + + tx.Commit(); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + Owner owner = s.Get(ownerId); + + Assert.NotNull(owner.Details); + } + } + + [Test] + public void OneToOnePersistedOnOwnerUpdateForSessionUpdate() + { + Owner owner; + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + owner = new Owner() + { + Name = "Owner", + }; + + s.Save(owner); + tx.Commit(); + } + + using (var s = Sfi.OpenSession()) + { + owner = s.Get(owner.Id); + } + + using (var s = Sfi.OpenSession()) + using (var tx = s.BeginTransaction()) + { + s.SaveOrUpdate(owner); + owner.Details = new Details() + { + Data = "Owner Details" + }; + + tx.Commit(); + } + + using (var s = Sfi.OpenSession()) + { + owner = s.Get(owner.Id); + + Assert.IsNotNull(owner.Details); + } + } + } +} diff --git a/src/NHibernate.Test/OneToOneType/Mappings.hbm.xml b/src/NHibernate.Test/OneToOneType/Mappings.hbm.xml new file mode 100644 index 00000000000..779ebae85fd --- /dev/null +++ b/src/NHibernate.Test/OneToOneType/Mappings.hbm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + Owner + + + + + + diff --git a/src/NHibernate.Test/OneToOneType/Owner.cs b/src/NHibernate.Test/OneToOneType/Owner.cs new file mode 100644 index 00000000000..d810f1fb4e6 --- /dev/null +++ b/src/NHibernate.Test/OneToOneType/Owner.cs @@ -0,0 +1,26 @@ +namespace NHibernate.Test.OneToOneType +{ + public class Owner + { + private Details _details; + + public virtual int Id { get; protected set; } + public virtual string Name { get; set; } + public virtual Details Details + { + get + { + return _details; + } + set + { + _details = value; + + if (_details != null) + { + _details.Owner = this; + } + } + } + } +} diff --git a/src/NHibernate/Async/Type/OneToOneType.cs b/src/NHibernate/Async/Type/OneToOneType.cs index d53f9c968d2..4bf56e41117 100644 --- a/src/NHibernate/Async/Type/OneToOneType.cs +++ b/src/NHibernate/Async/Type/OneToOneType.cs @@ -46,20 +46,29 @@ public override async Task NullSafeSetAsync(DbCommand cmd, object value, int ind .NullSafeSetAsync(cmd, await (GetReferenceValueAsync(value, session, cancellationToken)).ConfigureAwait(false), index, session, cancellationToken)).ConfigureAwait(false); } - public override Task IsDirtyAsync(object old, object current, ISessionImplementor session, CancellationToken cancellationToken) + public override async Task IsDirtyAsync(object old, object current, ISessionImplementor session, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) + cancellationToken.ThrowIfCancellationRequested(); + if (IsSame(old, current)) { - return Task.FromCanceled(cancellationToken); + return false; } - try + + if (old == null || current == null) { - return Task.FromResult(IsDirty(old, current, session)); + return true; } - catch (Exception ex) + + if ((await (ForeignKeys.IsTransientFastAsync(GetAssociatedEntityName(), current, session, cancellationToken)).ConfigureAwait(false)).GetValueOrDefault()) { - return Task.FromException(ex); + return true; } + + object oldId = await (GetIdentifierAsync(old, session, cancellationToken)).ConfigureAwait(false); + object newId = await (GetIdentifierAsync(current, session, cancellationToken)).ConfigureAwait(false); + IType identifierType = GetIdentifierType(session); + + return await (identifierType.IsDirtyAsync(oldId, newId, session, cancellationToken)).ConfigureAwait(false); } public override Task IsDirtyAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken) @@ -68,30 +77,24 @@ public override Task IsDirtyAsync(object old, object current, bool[] check { return Task.FromCanceled(cancellationToken); } - try - { - return Task.FromResult(IsDirty(old, current, checkable, session)); - } - catch (Exception ex) - { - return Task.FromException(ex); - } + return this.IsDirtyAsync(old, current, session, cancellationToken); } - public override Task IsModifiedAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken) + public override async Task IsModifiedAsync(object old, object current, bool[] checkable, ISessionImplementor session, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - try + cancellationToken.ThrowIfCancellationRequested(); + if (current == null) { - return Task.FromResult(IsModified(old, current, checkable, session)); + return old != null; } - catch (Exception ex) + if (old == null) { - return Task.FromException(ex); + return true; } + var oldIdentifier = IsIdentifier(old, session) ? old : await (GetIdentifierAsync(old, session, cancellationToken)).ConfigureAwait(false); + var currentIdentifier = await (GetIdentifierAsync(current, session, cancellationToken)).ConfigureAwait(false); + // the ids are fully resolved, so compare them with isDirty(), not isModified() + return await (GetIdentifierOrUniqueKeyType(session.Factory).IsDirtyAsync(oldIdentifier, currentIdentifier, session, cancellationToken)).ConfigureAwait(false); } public override async Task HydrateAsync(DbDataReader rs, string[] names, ISessionImplementor session, object owner, CancellationToken cancellationToken) @@ -123,39 +126,36 @@ public override async Task HydrateAsync(DbDataReader rs, string[] names, return identifier; } - public override Task DisassembleAsync(object value, ISessionImplementor session, object owner, CancellationToken cancellationToken) + public override async Task DisassembleAsync(object value, ISessionImplementor session, object owner, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - try + cancellationToken.ThrowIfCancellationRequested(); + if (value == null) { - return Task.FromResult(Disassemble(value, session, owner)); + return null; } - catch (Exception ex) + + object id = await (ForeignKeys.GetEntityIdentifierIfNotUnsavedAsync(GetAssociatedEntityName(), value, session, cancellationToken)).ConfigureAwait(false); + + if (id == null) { - return Task.FromException(ex); + throw new AssertionFailure("cannot cache a reference to an object with a null id: " + GetAssociatedEntityName()); } + + return await (GetIdentifierType(session).DisassembleAsync(id, session, owner, cancellationToken)).ConfigureAwait(false); } - public override Task AssembleAsync(object cached, ISessionImplementor session, object owner, CancellationToken cancellationToken) + public override async Task AssembleAsync(object cached, ISessionImplementor session, object owner, CancellationToken cancellationToken) { - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } - try - { - //this should be a call to resolve(), not resolveIdentifier(), - //'cos it might be a property-ref, and we did not cache the - //referenced value - return ResolveIdentifierAsync(session.GetContextEntityIdentifier(owner), session, owner, cancellationToken); - } - catch (Exception ex) + cancellationToken.ThrowIfCancellationRequested(); + // the owner of the association is not the owner of the id + object id = await (GetIdentifierType(session).AssembleAsync(cached, session, null, cancellationToken)).ConfigureAwait(false); + + if (id == null) { - return Task.FromException(ex); + return null; } + + return await (ResolveIdentifierAsync(id, session, cancellationToken)).ConfigureAwait(false); } } } diff --git a/src/NHibernate/Type/OneToOneType.cs b/src/NHibernate/Type/OneToOneType.cs index 0837ea3b98a..1400dacc57b 100644 --- a/src/NHibernate/Type/OneToOneType.cs +++ b/src/NHibernate/Type/OneToOneType.cs @@ -59,17 +59,57 @@ public override bool IsOneToOne public override bool IsDirty(object old, object current, ISessionImplementor session) { - return false; + if (IsSame(old, current)) + { + return false; + } + + if (old == null || current == null) + { + return true; + } + + if (ForeignKeys.IsTransientFast(GetAssociatedEntityName(), current, session).GetValueOrDefault()) + { + return true; + } + + object oldId = GetIdentifier(old, session); + object newId = GetIdentifier(current, session); + IType identifierType = GetIdentifierType(session); + + return identifierType.IsDirty(oldId, newId, session); } public override bool IsDirty(object old, object current, bool[] checkable, ISessionImplementor session) { - return false; + return this.IsDirty(old, current, session); } public override bool IsModified(object old, object current, bool[] checkable, ISessionImplementor session) { - return false; + if (current == null) + { + return old != null; + } + if (old == null) + { + return true; + } + var oldIdentifier = IsIdentifier(old, session) ? old : GetIdentifier(old, session); + var currentIdentifier = GetIdentifier(current, session); + // the ids are fully resolved, so compare them with isDirty(), not isModified() + return GetIdentifierOrUniqueKeyType(session.Factory).IsDirty(oldIdentifier, currentIdentifier, session); + } + + private bool IsIdentifier(object value, ISessionImplementor session) + { + var identifierType = GetIdentifierType(session); + if (identifierType == null) + { + return false; + } + return value.GetType() == identifierType.ReturnedClass; } public override bool IsNull(object owner, ISessionImplementor session) @@ -135,25 +175,40 @@ public override bool UseLHSPrimaryKey public override object Disassemble(object value, ISessionImplementor session, object owner) { - return null; + if (value == null) + { + return null; + } + + object id = ForeignKeys.GetEntityIdentifierIfNotUnsaved(GetAssociatedEntityName(), value, session); + + if (id == null) + { + throw new AssertionFailure("cannot cache a reference to an object with a null id: " + GetAssociatedEntityName()); + } + + return GetIdentifierType(session).Disassemble(id, session, owner); } public override object Assemble(object cached, ISessionImplementor session, object owner) { - //this should be a call to resolve(), not resolveIdentifier(), - //'cos it might be a property-ref, and we did not cache the - //referenced value - return ResolveIdentifier(session.GetContextEntityIdentifier(owner), session, owner); + // the owner of the association is not the owner of the id + object id = GetIdentifierType(session).Assemble(cached, session, null); + + if (id == null) + { + return null; + } + + return ResolveIdentifier(id, session); } /// - /// We don't need to dirty check one-to-one because of how - /// assemble/disassemble is implemented and because a one-to-one - /// association is never dirty + /// We only need to dirty check when the identifier can be null. /// public override bool IsAlwaysDirtyChecked { - get { return false; } //TODO: this is kinda inconsistent with CollectionType + get { return IsNullable; } } public override string PropertyName