From 54d794f58cc95119021303e8534502ecc0a26eaf Mon Sep 17 00:00:00 2001 From: dennis-gr Date: Tue, 19 Mar 2024 18:40:00 +0100 Subject: [PATCH 1/2] Add ToDictionaryAsync and ToHashSetAsync Linq extension methods --- .../Async/Linq/SelectionTests.cs | 21 +++ src/NHibernate.Test/Linq/SelectionTests.cs | 61 +++++++ src/NHibernate/Linq/LinqExtensionMethods.cs | 155 ++++++++++++++++++ 3 files changed, 237 insertions(+) diff --git a/src/NHibernate.Test/Async/Linq/SelectionTests.cs b/src/NHibernate.Test/Async/Linq/SelectionTests.cs index 8d3e0556cc8..b031a77b5b0 100644 --- a/src/NHibernate.Test/Async/Linq/SelectionTests.cs +++ b/src/NHibernate.Test/Async/Linq/SelectionTests.cs @@ -517,10 +517,31 @@ public async Task CanCastToCustomRegisteredTypeAsync() Assert.That(await (db.Users.Where(o => (NullableInt32) o.Id == 1).ToListAsync()), Has.Count.EqualTo(1)); } + [Test] + public async Task ToHashSetAsync() + { + var hashSet = await (db.Employees.Select(employee => employee.TitleOfCourtesy).ToHashSetAsync()); + Assert.That(hashSet.Count, Is.EqualTo(4)); + } + + [Test] + public async Task ToHashSet_With_ComparerAsync() + { + var hashSet = await (db.Employees.Select(employee => employee.Address).ToHashSetAsync(new AddressByCountryComparer())); + Assert.That(hashSet.Count, Is.EqualTo(2)); + } + public class Wrapper { public T item; public string message; } + + private class AddressByCountryComparer : IEqualityComparer
+ { + public bool Equals(Address x, Address y) => x?.Country == y?.Country; + + public int GetHashCode(Address obj) => obj.Country.GetHashCode(); + } } } diff --git a/src/NHibernate.Test/Linq/SelectionTests.cs b/src/NHibernate.Test/Linq/SelectionTests.cs index 6114e38c44d..9b87850acda 100644 --- a/src/NHibernate.Test/Linq/SelectionTests.cs +++ b/src/NHibernate.Test/Linq/SelectionTests.cs @@ -556,10 +556,71 @@ public void CanCastToCustomRegisteredType() Assert.That(db.Users.Where(o => (NullableInt32) o.Id == 1).ToList(), Has.Count.EqualTo(1)); } + [Test] + public void ToHashSet() + { + var hashSet = db.Employees.Select(employee => employee.TitleOfCourtesy).ToHashSet(); + Assert.That(hashSet.Count, Is.EqualTo(4)); + } + + [Test] + public void ToHashSet_With_Comparer() + { + var hashSet = db.Employees.Select(employee => employee.Address).ToHashSet(new AddressByCountryComparer()); + Assert.That(hashSet.Count, Is.EqualTo(2)); + } + + [Test] + public void ToDictionary() + { + var dictionary = db.Employees.OrderBy(e => e.EmployeeId).Take(3).ToDictionary(e => e.EmployeeId); + Assert.Multiple(() => + { + Assert.That(dictionary.Count, Is.EqualTo(3)); + + Assert.That(dictionary[1].EmployeeId, Is.EqualTo(1)); + Assert.That(dictionary[2].EmployeeId, Is.EqualTo(2)); + Assert.That(dictionary[3].EmployeeId, Is.EqualTo(3)); + }); + } + + [Test] + public void ToDictionary_With_Element_Selector() + { + var dictionary = db.Employees.OrderBy(e => e.EmployeeId).Take(3).ToDictionary(e => e.Address.PostalCode, e => e.FirstName); + Assert.Multiple(() => + { + Assert.That(dictionary.Count, Is.EqualTo(3)); + + Assert.That(dictionary["98122"], Is.EqualTo("Nancy")); + Assert.That(dictionary["98401"], Is.EqualTo("Andrew")); + Assert.That(dictionary["98033"], Is.EqualTo("Janet")); + }); + } + + [Test] + public void ToDictionary_With_Element_Selector_And_Comparer() + { + Assert.Throws(() => db.Employees.ToDictionary(e => e.Address, e => e.FirstName, new AddressByCountryComparer()), "An item with the same key has already been added."); + } + + [Test] + public void ToDictionary_With_Comparer() + { + Assert.Throws(() => db.Employees.ToDictionary(e => e.Address, new AddressByCountryComparer()), "An item with the same key has already been added."); + } + public class Wrapper { public T item; public string message; } + + private class AddressByCountryComparer : IEqualityComparer
+ { + public bool Equals(Address x, Address y) => x?.Country == y?.Country; + + public int GetHashCode(Address obj) => obj.Country.GetHashCode(); + } } } diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index 12b84e6e18c..dee36a3afc8 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -2400,6 +2400,161 @@ async Task> InternalToListAsync() #endregion + #region ToHashSetAsync + + /// + /// Executes the query and returns its result as a . + /// + /// An to return a HashSet from. + /// A cancellation token that can be used to cancel the work. + /// The type of the elements of . + /// A task that represents the asynchronous operation. + /// The task result contains a that contains elements from the input sequence. + /// is . + /// is not a . + public static Task> ToHashSetAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) => ToHashSetAsync(source, null, cancellationToken); + + /// + /// Executes the query and returns its result as a . + /// + /// An to return a HashSet from. + /// The implementation to use when comparing values in the set, or null to use the default implementation for the set type. + /// A cancellation token that can be used to cancel the work. + /// The type of the elements of . + /// + /// A task that represents the asynchronous operation. + /// The task result contains a that contains elements from the input sequence. + /// + /// is . + /// is not a . + public static async Task> ToHashSetAsync(this IQueryable source, IEqualityComparer comparer, CancellationToken cancellationToken = default(CancellationToken)) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + if (source.Provider is not INhQueryProvider) + { + throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); + } + + var hashSet = new HashSet(comparer); + foreach (var item in await source.ToListAsync(cancellationToken).ConfigureAwait(false)) + { + hashSet.Add(item); + } + + return hashSet; + } + + #endregion + + #region ToDictionaryAsync + + /// + /// Executes the query and returns its result as a according to a specified key selector function. + /// + /// The type of the elements of + /// The type of the key returned by + /// An to return a Dictionary from. + /// A function to extract a key from each element. + /// A cancellation token that can be used to cancel the work. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a that contains selected keys and values. + /// + /// is . + /// is not a . + public static Task> ToDictionaryAsync(this IQueryable source, Func keySelector, CancellationToken cancellationToken = default(CancellationToken)) + where TKey : notnull => ToDictionaryAsync(source, keySelector, e => e, comparer: null, cancellationToken); + + /// + /// Executes the query and returns its result as a according to a specified key selector function and a comparer. + /// + /// The type of the elements of + /// The type of the key returned by + /// An to return a Dictionary from. + /// A function to extract a key from each element. + /// An to compare keys. + /// A cancellation token that can be used to cancel the work. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a that contains selected keys and values. + /// + /// is . + /// is not a . + public static Task> ToDictionaryAsync(this IQueryable source, Func keySelector, IEqualityComparer comparer, CancellationToken cancellationToken = default(CancellationToken)) + where TKey : notnull => ToDictionaryAsync(source, keySelector, e => e, comparer, cancellationToken); + + /// + /// Executes the query and returns its result as a according to a specified key selector function and an element selector function. + /// + /// The type of the elements of + /// The type of the key returned by + /// The type of the value returned by . + /// An to return a Dictionary from. + /// A function to extract a key from each element. + /// A transform function to produce a result element value from each element. + /// A cancellation token that can be used to cancel the work. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a that contains selected keys and values. + /// + /// is . + /// is not a . + public static Task> ToDictionaryAsync(this IQueryable source, Func keySelector, Func elementSelector, CancellationToken cancellationToken = default(CancellationToken)) + where TKey : notnull => ToDictionaryAsync(source, keySelector, elementSelector, comparer: null, cancellationToken); + + /// + /// Executes the query and returns its result as a according to a specified key selector function, a comparer, and an element selector function. + /// + /// The type of the elements of + /// The type of the key returned by + /// The type of the value returned by . + /// An to return a Dictionary from. + /// A function to extract a key from each element. + /// A transform function to produce a result element value from each element. + /// An to compare keys. + /// A cancellation token that can be used to cancel the work. + /// + /// A task that represents the asynchronous operation. + /// The task result contains a that contains selected keys and values. + /// + /// is . + /// is not a . + /// is . + /// is . + public static async Task> ToDictionaryAsync(this IQueryable source, Func keySelector, Func elementSelector, IEqualityComparer comparer, + CancellationToken cancellationToken = default(CancellationToken)) where TKey : notnull + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + if (source.Provider is not INhQueryProvider) + { + throw new NotSupportedException($"Source {nameof(source.Provider)} must be a {nameof(INhQueryProvider)}"); + } + if (keySelector == null) + { + throw new ArgumentNullException(nameof(keySelector)); + } + if (elementSelector == null) + { + throw new ArgumentNullException(nameof(elementSelector)); + } + + var dictionary = new Dictionary(comparer); + foreach (var element in await source.ToListAsync(cancellationToken).ConfigureAwait(false)) + { + dictionary.Add(keySelector(element), elementSelector(element)); + } + + return dictionary; + } + + #endregion + /// /// Wraps the query in a deferred which enumeration will trigger a batch of all pending future queries. /// From 8f8a1dc50dbebc4ac63538f4e3c1fe882ad7bb0b Mon Sep 17 00:00:00 2001 From: dennis-gr Date: Sun, 5 May 2024 15:59:39 +0200 Subject: [PATCH 2/2] Add note about intermediate list allocation --- src/NHibernate/Linq/LinqExtensionMethods.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/NHibernate/Linq/LinqExtensionMethods.cs b/src/NHibernate/Linq/LinqExtensionMethods.cs index dee36a3afc8..89c024f612a 100644 --- a/src/NHibernate/Linq/LinqExtensionMethods.cs +++ b/src/NHibernate/Linq/LinqExtensionMethods.cs @@ -2405,6 +2405,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a . /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// An to return a HashSet from. /// A cancellation token that can be used to cancel the work. /// The type of the elements of . @@ -2417,6 +2420,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a . /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// An to return a HashSet from. /// The implementation to use when comparing values in the set, or null to use the default implementation for the set type. /// A cancellation token that can be used to cancel the work. @@ -2454,6 +2460,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a according to a specified key selector function. /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// The type of the elements of /// The type of the key returned by /// An to return a Dictionary from. @@ -2471,6 +2480,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a according to a specified key selector function and a comparer. /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// The type of the elements of /// The type of the key returned by /// An to return a Dictionary from. @@ -2489,6 +2501,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a according to a specified key selector function and an element selector function. /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// The type of the elements of /// The type of the key returned by /// The type of the value returned by . @@ -2508,6 +2523,9 @@ async Task> InternalToListAsync() /// /// Executes the query and returns its result as a according to a specified key selector function, a comparer, and an element selector function. /// + /// + /// Note that this method still allocates an intermediate instance from which the result is created. + /// /// The type of the elements of /// The type of the key returned by /// The type of the value returned by .