Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix declared type of custom comparer for MaxBy and MinBy #113944

Merged
merged 3 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
| __`SYSLIB0058`__ | KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherStrength, HashAlgorithm and HashStrength properties of SslStream are obsolete. Use NegotiatedCipherSuite instead. |
| __`SYSLIB0059`__ | SystemEvents.EventsThreadShutdown callbacks are not run before the process exits. Use AppDomain.ProcessExit instead. |
| __`SYSLIB0060`__ | The constructors on Rfc2898DeriveBytes are obsolete. Use the static Pbkdf2 method instead. |
| __`SYSLIB0061`__ | The Queryable MinBy and MaxBy taking an IComparer\<TSource\> are obsolete. Use the new ones that take an IComparer\<TKey\>. |

## Analyzer Warnings

Expand Down Expand Up @@ -185,7 +186,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
| __`SYSLIB1059`__ | Marshaller type does not support allocating constructor |
| __`SYSLIB1060`__ | Specified marshaller type is invalid |
| __`SYSLIB1061`__ | Marshaller type has incompatible method signatures |
| __`SYSLIB1062`__ | Project must be updated with '<AllowUnsafeBlocks>true</AllowUnsafeBlocks>' |
| __`SYSLIB1062`__ | Project must be updated with '\<AllowUnsafeBlocks\>true\</AllowUnsafeBlocks\>' |
| __`SYSLIB1063`__ | _`SYSLIB1063`-`SYSLIB1069` reserved for Microsoft.Interop.LibraryImportGenerator._ |
| __`SYSLIB1064`__ | _`SYSLIB1063`-`SYSLIB1069` reserved for Microsoft.Interop.LibraryImportGenerator._ |
| __`SYSLIB1065`__ | _`SYSLIB1063`-`SYSLIB1069` reserved for Microsoft.Interop.LibraryImportGenerator._ |
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Common/src/System/Obsoletions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ internal static class Obsoletions
internal const string Rfc2898DeriveBytesCtorMessage = "The constructors on Rfc2898DeriveBytes are obsolete. Use the static Pbkdf2 method instead.";
internal const string Rfc2898DeriveBytesCtorDiagId = "SYSLIB0060";

internal const string QueryableMinByMaxByTSourceObsoleteMessage = "The Queryable MinBy and MaxBy taking an IComparer<TSource> are obsolete. Use the new ones that take an IComparer<TKey>.";
internal const string QueryableMinByMaxByTSourceObsoleteDiagId = "SYSLIB0061";

// When adding a new diagnostic ID, add it to the table in docs\project\list-of-diagnostics.md as well.
// Keep new const identifiers above this comment.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,20 @@ public static partial class Queryable
public static long LongCount<TSource>(this System.Linq.IQueryable<TSource> source) { throw null; }
public static long LongCount<TSource>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, bool>> predicate) { throw null; }
public static TSource? MaxBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector) { throw null; }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)]
[System.ObsoleteAttribute("The Queryable MinBy and MaxBy taking an IComparer<TSource> are obsolete. Use the new ones that take an IComparer<TKey>.", DiagnosticId="SYSLIB0061", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public static TSource? MaxBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TSource>? comparer) { throw null; }
public static TSource? MaxBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TKey>? comparer) { throw null; }
public static TSource? Max<TSource>(this System.Linq.IQueryable<TSource> source) { throw null; }
public static TSource? Max<TSource>(this System.Linq.IQueryable<TSource> source, System.Collections.Generic.IComparer<TSource>? comparer) { throw null; }
public static TResult? Max<TSource, TResult>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TResult>> selector) { throw null; }
public static TSource? MinBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector) { throw null; }
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute(-1)]
[System.ObsoleteAttribute("The Queryable MinBy and MaxBy taking an IComparer<TSource> are obsolete. Use the new ones that take an IComparer<TKey>.", DiagnosticId="SYSLIB0061", UrlFormat="https://aka.ms/dotnet-warnings/{0}")]
public static TSource? MinBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TSource>? comparer) { throw null; }
public static TSource? MinBy<TSource, TKey>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TKey>> keySelector, System.Collections.Generic.IComparer<TKey>? comparer) { throw null; }
public static TSource? Min<TSource>(this System.Linq.IQueryable<TSource> source) { throw null; }
public static TSource? Min<TSource>(this System.Linq.IQueryable<TSource> source, System.Collections.Generic.IComparer<TSource>? comparer) { throw null; }
public static TResult? Min<TSource, TResult>(this System.Linq.IQueryable<TSource> source, System.Linq.Expressions.Expression<System.Func<TSource, TResult>> selector) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@
<Reference Include="System.Runtime" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(CommonPath)System\Obsoletions.cs"
Link="Common\System\Obsoletions.cs" />
</ItemGroup>

</Project>
64 changes: 60 additions & 4 deletions src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

namespace System.Linq
{
Expand Down Expand Up @@ -1780,11 +1782,14 @@ public static long LongCount<TSource>(this IQueryable<TSource> source, Expressio
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the minimum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The <see cref="IComparer{TKey}" /> to compare keys.</param>
/// <param name="comparer">The <see cref="IComparer{TSource}" /> to compare elements.</param>
/// <returns>The value with the minimum key in the sequence.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TSource}" /> interface.</exception>
[DynamicDependency("MinBy`2", typeof(Enumerable))]
[Obsolete(Obsoletions.QueryableMinByMaxByTSourceObsoleteMessage, DiagnosticId=Obsoletions.QueryableMinByMaxByTSourceObsoleteDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
[EditorBrowsable(EditorBrowsableState.Never)]
[OverloadResolutionPriority(-1)]
public static TSource? MinBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, IComparer<TSource>? comparer)
{
ArgumentNullException.ThrowIfNull(source);
Expand All @@ -1799,6 +1804,30 @@ public static long LongCount<TSource>(this IQueryable<TSource> source, Expressio
Expression.Constant(comparer, typeof(IComparer<TSource>))));
}

/// <summary>Returns the minimum value in a generic <see cref="IQueryable{T}"/> according to a specified key selector function.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the minimum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The <see cref="IComparer{TKey}" /> to compare keys.</param>
/// <returns>The value with the minimum key in the sequence.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
[DynamicDependency("MinBy`2", typeof(Enumerable))]
public static TSource? MinBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, IComparer<TKey>? comparer)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);

return source.Provider.Execute<TSource>(
Expression.Call(
null,
new Func<IQueryable<TSource>, Expression<Func<TSource, TKey>>, IComparer<TKey>, TSource?>(MinBy).Method,
source.Expression,
Expression.Quote(keySelector),
Expression.Constant(comparer, typeof(IComparer<TKey>))));
}

[DynamicDependency("Max`1", typeof(Enumerable))]
public static TSource? Max<TSource>(this IQueryable<TSource> source)
{
Expand Down Expand Up @@ -1870,11 +1899,14 @@ public static long LongCount<TSource>(this IQueryable<TSource> source, Expressio
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the maximum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The <see cref="IComparer{TKey}" /> to compare keys.</param>
/// <param name="comparer">The <see cref="IComparer{TSource}" /> to compare elements.</param>
/// <returns>The value with the maximum key in the sequence.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TSource}" /> interface.</exception>
[DynamicDependency("MaxBy`2", typeof(Enumerable))]
[Obsolete(Obsoletions.QueryableMinByMaxByTSourceObsoleteMessage, DiagnosticId=Obsoletions.QueryableMinByMaxByTSourceObsoleteDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
[EditorBrowsable(EditorBrowsableState.Never)]
[OverloadResolutionPriority(-1)]
public static TSource? MaxBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, IComparer<TSource>? comparer)
{
ArgumentNullException.ThrowIfNull(source);
Expand All @@ -1889,6 +1921,30 @@ public static long LongCount<TSource>(this IQueryable<TSource> source, Expressio
Expression.Constant(comparer, typeof(IComparer<TSource>))));
}

/// <summary>Returns the maximum value in a generic <see cref="IQueryable{T}"/> according to a specified key selector function.</summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
/// <typeparam name="TKey">The type of key to compare elements by.</typeparam>
/// <param name="source">A sequence of values to determine the maximum value of.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <param name="comparer">The <see cref="IComparer{TKey}" /> to compare keys.</param>
/// <returns>The value with the maximum key in the sequence.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">No key extracted from <paramref name="source" /> implements the <see cref="IComparable" /> or <see cref="IComparable{TKey}" /> interface.</exception>
[DynamicDependency("MaxBy`2", typeof(Enumerable))]
public static TSource? MaxBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, IComparer<TKey>? comparer)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);

return source.Provider.Execute<TSource>(
Expression.Call(
null,
new Func<IQueryable<TSource>, Expression<Func<TSource, TKey>>, IComparer<TKey>, TSource?>(MaxBy).Method,
source.Expression,
Expression.Quote(keySelector),
Expression.Constant(comparer, typeof(IComparer<TKey>))));
}

[DynamicDependency("Sum", typeof(Enumerable))]
public static int Sum(this IQueryable<int> source)
{
Expand Down
21 changes: 21 additions & 0 deletions src/libraries/System.Linq.Queryable/tests/MaxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -638,5 +638,26 @@ public void MaxBy_CustomComparer()
IQueryable<int> source = Enumerable.Range(1, 20).AsQueryable();
Assert.Equal(20, source.MaxBy(x => -x, Comparer<int>.Create((x, y) => -x.CompareTo(y))));
}

private sealed class Folk
{
public string Name { get; set; }
public int Age { get; set; }
}

[Fact]
public void MaxBy_CustomComparer_DistinctTypes()
{
var data = new Folk[] {
new Folk { Name="Doug", Age=42 },
new Folk { Name="John", Age=18 },
new Folk { Name="Bob", Age=21 }
};

IQueryable<Folk> source = data.AsQueryable();
var result = source.MaxBy(e => e.Age, Comparer<int>.Default);
Assert.NotNull(result);
Assert.Equal("Doug", result.Name);
}
}
}
21 changes: 21 additions & 0 deletions src/libraries/System.Linq.Queryable/tests/MinTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,5 +606,26 @@ public void MinBy_CustomComparer()
IQueryable<int> source = Enumerable.Range(1, 20).AsQueryable();
Assert.Equal(1, source.MinBy(x => -x, Comparer<int>.Create((x, y) => -x.CompareTo(y))));
}

private sealed class Folk
{
public string Name { get; set; }
public int Age { get; set; }
}

[Fact]
public void MinBy_CustomComparer_DistinctTypes()
{
var data = new Folk[] {
new Folk { Name="Doug", Age=42 },
new Folk { Name="John", Age=18 },
new Folk { Name="Bob", Age=21 }
};

IQueryable<Folk> source = data.AsQueryable();
var result = source.MinBy(e => e.Age, Comparer<int>.Default);
Assert.NotNull(result);
Assert.Equal("John", result.Name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,4 @@
<Left>net9.0/System.Runtime.dll</Left>
<Right>net10.0/System.Runtime.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>
Loading