From bf4870a0c0dbcce9dd91087c455af1717af7fbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Fri, 4 Sep 2026 13:48:45 +0200 Subject: [PATCH 1/2] Add test for ToListAsync() on 'IQueryable' Currently fails with this compilation error: > No overload for method 'ToList' takes 1 arguments --- tests/Generator.Tests/ExtensionMethodTests.cs | 22 +++++++++++++++++++ ...3d48c88.QueryableToListAsync.g.verified.cs | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/Generator.Tests/Snapshots/ExtensionMethodTests.EntityFrameworkQueryableToListAsync#Zomp.SyncMethodGenerator.IntegrationTests.EntityFrameworkQueryabl_b3d48c88.QueryableToListAsync.g.verified.cs diff --git a/tests/Generator.Tests/ExtensionMethodTests.cs b/tests/Generator.Tests/ExtensionMethodTests.cs index 1ff1d3c..6311e26 100644 --- a/tests/Generator.Tests/ExtensionMethodTests.cs +++ b/tests/Generator.Tests/ExtensionMethodTests.cs @@ -144,6 +144,28 @@ public async Task QueryableExtensionAsync(DbContext dbContext, Cancellation } } } +""".Verify(sourceType: SourceType.Full); + + [Fact] + public Task EntityFrameworkQueryableToListAsync() => """ +using System.Threading; +using System.Threading.Tasks; + +namespace Zomp.SyncMethodGenerator.IntegrationTests +{ + using Microsoft.EntityFrameworkCore; + + public partial class EntityFrameworkQueryableExtensions + { + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public async Task QueryableToListAsync(DbContext dbContext, CancellationToken cancellationToken) + { + var dbSet = dbContext.Set(); + var result = await dbSet.ToListAsync(cancellationToken); + return result.Count; + } + } +} """.Verify(sourceType: SourceType.Full); #endif diff --git a/tests/Generator.Tests/Snapshots/ExtensionMethodTests.EntityFrameworkQueryableToListAsync#Zomp.SyncMethodGenerator.IntegrationTests.EntityFrameworkQueryabl_b3d48c88.QueryableToListAsync.g.verified.cs b/tests/Generator.Tests/Snapshots/ExtensionMethodTests.EntityFrameworkQueryableToListAsync#Zomp.SyncMethodGenerator.IntegrationTests.EntityFrameworkQueryabl_b3d48c88.QueryableToListAsync.g.verified.cs new file mode 100644 index 0000000..dbf5ba7 --- /dev/null +++ b/tests/Generator.Tests/Snapshots/ExtensionMethodTests.EntityFrameworkQueryableToListAsync#Zomp.SyncMethodGenerator.IntegrationTests.EntityFrameworkQueryabl_b3d48c88.QueryableToListAsync.g.verified.cs @@ -0,0 +1,21 @@ +//HintName: Zomp.SyncMethodGenerator.IntegrationTests.EntityFrameworkQueryabl_b3d48c88.QueryableToListAsync.g.cs +// +#nullable enable + +using System.Threading; +using System.Threading.Tasks; + +namespace Zomp.SyncMethodGenerator.IntegrationTests +{ + using Microsoft.EntityFrameworkCore; + + public partial class EntityFrameworkQueryableExtensions + { + public int QueryableToList(global::Microsoft.EntityFrameworkCore.DbContext dbContext) + { + var dbSet = dbContext.Set(); + var result = global::System.Linq.Enumerable.ToList(dbSet); + return result.Count; + } + } +} From 290415162d27fbed17e8fa756142d4baa01f793c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Mon, 7 Sep 2026 11:27:32 +0200 Subject: [PATCH 2/2] Fix LINQ async to sync translation for extensions After #108, #110 and #123 let's hope it's the definitive fix. --- .../AsyncToSyncRewriter.cs | 30 ++++++++++--------- .../Helpers/CompilationExtensions.cs | 22 ++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 src/Zomp.SyncMethodGenerator/Helpers/CompilationExtensions.cs diff --git a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs index f975add..b8a0135 100644 --- a/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs +++ b/src/Zomp.SyncMethodGenerator/AsyncToSyncRewriter.cs @@ -1516,7 +1516,6 @@ private static string MakeType(ISymbol symbol) => symbol switch { INamedTypeSymbol { Name: "AsyncEnumerable" } => Global("System.Linq.Enumerable"), - INamedTypeSymbol { Name: "EntityFrameworkQueryableExtensions" } => Global("System.Linq.Queryable"), INamedTypeSymbol => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), _ => symbol.Name, }; @@ -2067,19 +2066,22 @@ private InvocationExpressionSyntax UnwrapExtension(InvocationExpressionSyntax ie var newName = reducedFrom.Name; newName = changeMemoryToSpan ? ReplaceWithSpan(reducedFrom) : RemoveAsync(newName); - var membersWithNewNameInContainingType = semanticModel.Compilation.References - .Select(semanticModel.Compilation.GetAssemblyOrModuleSymbol) - .Append(semanticModel.Compilation.Assembly) - .OfType() - .Select(assemblySymbol => assemblySymbol.GetTypeByMetadataName(containingType.ToString())) - .OfType() - .SelectMany(symbol => symbol.GetMembers(newName)); - - // When the method is an AsyncEnumerable extension it must be converted to the corresponding Enumerable extension - // regardless of the containing type featuring members with compatible names - var fullyQualifiedName = !containingType.Name.Equals("AsyncEnumerable", StringComparison.Ordinal) && membersWithNewNameInContainingType.Any() - ? $"{containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{newName}" - : $"{MakeType(containingType)}.{newName}"; + var (enumerableMembers, queryableMembers) = semanticModel.Compilation.GetLinqMembers(); + + var fullyQualifiedName = $"{MakeType(containingType)}.{newName}"; + + // Supports EntityFrameworkQueryableExtensions and potentially other queryable extensions + if (containingType.Name.EndsWith("QueryableExtensions", StringComparison.OrdinalIgnoreCase)) + { + if (queryableMembers.Contains(newName)) + { + fullyQualifiedName = $"{Global("System.Linq.Queryable")}.{newName}"; + } + else if (enumerableMembers.Contains(newName)) + { + fullyQualifiedName = $"{Global("System.Linq.Enumerable")}.{newName}"; + } + } var es = (ies.Expression switch { diff --git a/src/Zomp.SyncMethodGenerator/Helpers/CompilationExtensions.cs b/src/Zomp.SyncMethodGenerator/Helpers/CompilationExtensions.cs new file mode 100644 index 0000000..ed8dcc2 --- /dev/null +++ b/src/Zomp.SyncMethodGenerator/Helpers/CompilationExtensions.cs @@ -0,0 +1,22 @@ +namespace Zomp.SyncMethodGenerator.Helpers; + +internal static class CompilationExtensions +{ + public static (ISet EnumerableMembers, ISet QueryableMembers) GetLinqMembers(this Compilation compilation) + { + INamedTypeSymbol? linqEnumerable = null; + INamedTypeSymbol? linqQueryable = null; + foreach (var reference in compilation.References) + { + var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; + linqEnumerable ??= assemblySymbol?.GetTypeByMetadataName("System.Linq.Enumerable"); + linqQueryable ??= assemblySymbol?.GetTypeByMetadataName("System.Linq.Queryable"); + if (linqEnumerable != null && linqQueryable != null) + { + break; + } + } + + return (new HashSet(linqEnumerable?.MemberNames ?? []), new HashSet(linqQueryable?.MemberNames ?? [])); + } +}