From e81cb9fa2f359e73966c6fe5f6a5895b216af880 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Tue, 17 Mar 2026 12:14:49 +0500 Subject: [PATCH 1/3] Add SingleOrDefaultAsync overload with single key value --- .../Prefetch/FetchByKeyWithCachingTest.cs | 49 ++++++++++++++++++- Orm/Xtensive.Orm/Orm/Query.cs | 17 +++++++ Orm/Xtensive.Orm/Orm/QueryEndpoint.cs | 29 +++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs b/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs index d8cc4bfe25..595229e181 100644 --- a/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs +++ b/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs @@ -327,7 +327,7 @@ public void SingleOrDefaultByExistingIdTest() } [Test] - public async Task SingleOrDefaultByExistingIdAsyncTest() + public async Task SingleOrDefaultByExistingIdAsyncTest1() { await RunWithinSessionAsync(async (s) => { var existingId = (int) existingKeys[customerType][0].Value.GetValue(0, out var _); @@ -348,6 +348,28 @@ await RunWithinSessionAsync(async (s) => { }); } + [Test] + public async Task SingleOrDefaultByExistingIdAsyncTest2() + { + await RunWithinSessionAsync(async (s) => { + var existingId = (int) existingKeys[customerType][0].Value.GetValue(0, out var _); + var detector = new QueryExecutionDetector(); + using (detector.Attach(s)) { + // Entity is not in cache yet + var existingEntity = await s.Query.SingleOrDefaultAsync(existingId); + } + Assert.That(detector.DbCommandsDetected, Is.True); + detector.Reset(); + + using (detector.Attach(s)) { + // now it is in cache + var existingEntity = await s.Query.SingleOrDefaultAsync(existingId); + } + Assert.That(detector.DbCommandsDetected, Is.False); + detector.Reset(); + }); + } + [Test] public void SingleOrDefaultByInexistentIdTest() { @@ -372,7 +394,7 @@ public void SingleOrDefaultByInexistentIdTest() } [Test] - public async Task SingleOrDefaultByInexistentIdAsyncTest() + public async Task SingleOrDefaultByInexistentIdAsyncTest1() { await RunWithinSessionAsync(async (s) => { var inexistentId = 9999; @@ -395,6 +417,29 @@ await RunWithinSessionAsync(async (s) => { }); } + [Test] + public async Task SingleOrDefaultByInexistentIdAsyncTest2() + { + await RunWithinSessionAsync(async (s) => { + var inexistentId = 9999; + + var detector = new QueryExecutionDetector(); + using (detector.Attach(s)) { + var shouldBeNull = await s.Query.SingleOrDefaultAsync(inexistentId); + Assert.That(shouldBeNull, Is.Null); + } + Assert.That(detector.DbCommandsDetected, Is.True); + detector.Reset(); + + using (detector.Attach(s)) { + var shouldBeNull = await s.Query.SingleOrDefaultAsync(inexistentId); + Assert.That(shouldBeNull, Is.Null); + } + Assert.That(detector.DbCommandsDetected, Is.False); + detector.Reset(); + await Task.CompletedTask; + }); + } private void RunWithinSession(Action testAction) { diff --git a/Orm/Xtensive.Orm/Orm/Query.cs b/Orm/Xtensive.Orm/Orm/Query.cs index add4f44839..4d7e726168 100644 --- a/Orm/Xtensive.Orm/Orm/Query.cs +++ b/Orm/Xtensive.Orm/Orm/Query.cs @@ -385,6 +385,23 @@ public static Task SingleOrDefaultAsync(object[] keyValues, CancellationTo return Session.Demand().Query.SingleOrDefaultAsync(keyValues, token); } + /// + /// Resolves (gets) the by the specified + /// in the current . + /// + /// Type of the entity. + /// Key value. + /// The token to cancel this operation. + /// + /// The specified identify. + /// , if there is no such entity. + /// + public static Task SingleOrDefaultAsync(object keyValue, CancellationToken token) + where T : class, IEntity + { + return Session.Demand().Query.SingleOrDefaultAsync(keyValue, token); + } + #region Execute /// diff --git a/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs b/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs index 3389dea52f..3f220d6fbd 100644 --- a/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs +++ b/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs @@ -496,6 +496,23 @@ public async Task SingleOrDefaultAsync(object[] keyValues, CancellationTok return (T) (object) (await SingleOrDefaultAsync(GetKeyByValues(keyValues), token).ConfigureAwait(false)); } + /// + /// Resolves (gets) the by the specified + /// in the current . + /// + /// Type of the entity. + /// Key value. + /// The token to cancel this operation. + /// + /// The specified identify. + /// , if there is no such entity. + /// + public async Task SingleOrDefaultAsync(object keyValue, CancellationToken token = default) + where T : class, IEntity + { + return (T) (object) (await SingleOrDefaultAsync(GetKeyByValue(keyValue), token).ConfigureAwait(false)); + } + /// /// Fetches multiple instances of specified type by provided . /// @@ -970,6 +987,18 @@ private Key GetKeyByValues(object[] keyValues) return Key.Create(session.Domain, session.StorageNodeId, typeof(T), TypeReferenceAccuracy.BaseType, keyValues); } + private Key GetKeyByValue(object keyValue) + { + ArgumentNullException.ThrowIfNull(keyValue); + switch (keyValue) { + case Key key: + return key; + case Entity entity: + return entity.Key; + } + return Key.Create(session.Domain, session.StorageNodeId, typeof(T), TypeReferenceAccuracy.BaseType, keyValue); + } + private Expression BuildRootExpression(Type elementType) { return RootBuilder!=null From 061cf327fdfaaea11a08614a4662f52f8db79e2c Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 11:42:13 +0500 Subject: [PATCH 2/3] QueryEndpoint.SingleAsync also has option for passing single key value --- .../Prefetch/FetchByKeyWithCachingTest.cs | 48 ++++++++++++++++++- Orm/Xtensive.Orm/Orm/Query.cs | 17 +++++++ Orm/Xtensive.Orm/Orm/QueryEndpoint.cs | 17 +++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs b/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs index 595229e181..8050903eec 100644 --- a/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs +++ b/Orm/Xtensive.Orm.Tests/Storage/Prefetch/FetchByKeyWithCachingTest.cs @@ -143,7 +143,7 @@ public void SingleByExistingIdTest() } [Test] - public async Task SingleByExistingIdAsyncTest() + public async Task SingleByExistingIdAsyncTest1() { await RunWithinSessionAsync(async (s) => { var existingId = (int) existingKeys[customerType][0].Value.GetValue(0, out var _); @@ -164,6 +164,28 @@ await RunWithinSessionAsync(async (s) => { }); } + [Test] + public async Task SingleByExistingIdAsyncTest2() + { + await RunWithinSessionAsync(async (s) => { + var existingId = (int) existingKeys[customerType][0].Value.GetValue(0, out var _); + var detector = new QueryExecutionDetector(); + using (detector.Attach(s)) { + // Entity is not in cache yet + var existingEntity = await s.Query.SingleAsync(existingId); + } + Assert.That(detector.DbCommandsDetected, Is.True); + detector.Reset(); + + using (detector.Attach(s)) { + // now it is in cache + var existingEntity = await s.Query.SingleAsync(existingId); + } + Assert.That(detector.DbCommandsDetected, Is.False); + detector.Reset(); + }); + } + [Test] public void SingleByInexistentIdTest() { @@ -186,7 +208,7 @@ public void SingleByInexistentIdTest() } [Test] - public async Task SingleByInexistentIdAsyncTest() + public async Task SingleByInexistentIdAsyncTest1() { await RunWithinSessionAsync(async (s) => { var inexistentId = 9999; @@ -207,6 +229,28 @@ await RunWithinSessionAsync(async (s) => { }); } + [Test] + public async Task SingleByInexistentIdAsyncTest2() + { + await RunWithinSessionAsync(async (s) => { + var inexistentId = 9999; + + var detector = new QueryExecutionDetector(); + using (detector.Attach(s)) { + _ = Assert.ThrowsAsync(async () => await s.Query.SingleAsync(inexistentId)); + } + Assert.That(detector.DbCommandsDetected, Is.True); + detector.Reset(); + + using (detector.Attach(s)) { + _ = Assert.ThrowsAsync(async () => await s.Query.SingleAsync(inexistentId)); + } + Assert.That(detector.DbCommandsDetected, Is.False); + detector.Reset(); + await Task.CompletedTask; + }); + } + [Test] public void SingleOrDefaultByExistingKeyTest() { diff --git a/Orm/Xtensive.Orm/Orm/Query.cs b/Orm/Xtensive.Orm/Orm/Query.cs index 4d7e726168..53a0b30199 100644 --- a/Orm/Xtensive.Orm/Orm/Query.cs +++ b/Orm/Xtensive.Orm/Orm/Query.cs @@ -316,6 +316,23 @@ public static Task SingleAsync(object[] keyValues, CancellationToken token return Session.Demand().Query.SingleAsync(keyValues, token); } + /// + /// Resolves (gets) the by the specified + /// in the current . + /// + /// Type of the entity. + /// Key value. + /// The token to cancel this operation. + /// + /// The specified identify. + /// + /// Entity with the specified key is not found. + public static Task SingleAsync(object keyValue, CancellationToken token) + where T : class, IEntity + { + return Session.Demand().Query.SingleAsync(keyValue, token); + } + /// /// Resolves (gets) the by the specified /// in the current . diff --git a/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs b/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs index 3f220d6fbd..365684bf51 100644 --- a/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs +++ b/Orm/Xtensive.Orm/Orm/QueryEndpoint.cs @@ -428,6 +428,23 @@ public async Task SingleAsync(object[] keyValues, CancellationToken token return (T) (object) (await SingleAsync(GetKeyByValues(keyValues), token).ConfigureAwait(false)); } + /// + /// Resolves (gets) the by the specified + /// in the current . + /// + /// Type of the entity. + /// Key value. + /// The token to cancel this operation. + /// + /// The specified identify. + /// + /// Entity with the specified key is not found. + public async Task SingleAsync(object keyValue, CancellationToken token = default) + where T : class, IEntity + { + return (T) (object) (await SingleAsync(GetKeyByValue(keyValue), token).ConfigureAwait(false)); + } + /// /// Resolves (gets) the by the specified /// in the current . From f6682faf0a88bcaafeb33d641f558d4fac4dcc2b Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Wed, 18 Mar 2026 12:11:55 +0500 Subject: [PATCH 3/3] Improve changlog --- ChangeLog/7.2.2-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog/7.2.2-dev.txt b/ChangeLog/7.2.2-dev.txt index 51851af91f..38b6795c50 100644 --- a/ChangeLog/7.2.2-dev.txt +++ b/ChangeLog/7.2.2-dev.txt @@ -1 +1,2 @@ -[main] Query.CreateDelayedQuery(key, Func>) applies external key instead of default computed, as it suppose to \ No newline at end of file +[main] Query.CreateDelayedQuery(key, Func>) applies external key instead of default computed, as it suppose to +[main] QueryEndpoint.SingleAsync()/SingleOrDefaultAsync() get overloads that can recieve one key value as parameter without need to create array explicitly \ No newline at end of file