I'm writing a Roslyn code analyzer to warn when a synchronous version of a method is used and there is an asynchronous version available.
I'm going to use it in the context of an ASP.NET Core MVC app which uses EF Core - making synchronous calls to the database causes thread exhaustion and the EF Core IQueryable extensions all use a convention to name the async version of the methods e.g. First has FirstAsync etc.
The first step is to detect synchronous methods, then to look for an asynchronous alternative with a compatible signature.
The problem I have is that when I have a synchronous method candidate, it is an InvocationExpressionSyntax
and I need to get an ISymbol
from that, but SemanticModel.GetSymbolInfo
only gives a simple result if there are no overloads. If there are overloads it returns CandidateReason.OverloadResolutionFailure
and a CandidateSymbols
array.
It seems weird to me that it can't just tell me which symbol the syntax resolves to. I found a conversation on Gitter from 2019 where they say "the language has no such concept you'd have to figure it out yourself". It also includes an example of code someone wrote to try to figure out how to deduce the overload manually by comparing the expression's arguments to the parameters of the candidate symbols, but I can't get it to work.
Is there a way for Roslyn to just tell me which symbol the syntax resolves to? If not, how do I do it manually?
Update 1:
The method call isn't ambiguous - the invocation expression is
context.Users.First(u => u.Id == 1)
and the candidate symbols are
DbSet<User>.First()
DbSet<User>.First(Expression<Func<User, bool>>)
Update 2:
The source I'm testing is
using System.Linq.Expressions;
public abstract class DbSet<TEntity> where TEntity : class {
public TEntity First() {
throw new Exception();
}
public TEntity First(Expression<Func<TEntity, bool>> predicate) {
throw new Exception();
}
public Task<TEntity> FirstAsync(CancellationToken cancellationToken = default) {
throw new Exception();
}
public Task<TEntity> FirstAsync<TSource>(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default) {
throw new Exception();
}
}
public class User {
public int Id { get; set; }
}
public class ApplicationDbContext {
public DbSet<User> Users { get; set; } = null!;
}
public class Test {
public void TestContext(ApplicationDbContext context) {
var x = context.Users.First();
var y = context.Users.First(u => u.Id == 1);
}
}
and the analyzer is:
private void AnalyzeSyncInvocationExpression(SyntaxNodeAnalysisContext context) {
var invocationExpr = (InvocationExpressionSyntax)context.Node;
// we only care about sync expressions
if (invocationExpr.Parent is AwaitExpressionSyntax) {
return;
}
// check if is complete expression - stop it from triggering on each part of the call chain
if (invocationExpr.Parent is MemberAccessExpressionSyntax) {
return;
}
// check if return value is IQueryable - stop it from triggering on query building
if (context.SemanticModel.GetTypeInfo(invocationExpr).Type is INamedTypeSymbol returnType && returnType.Name == nameof(IQueryable)) {
return;
}
// check if it is a sync method
var invokedSymbol = context.SemanticModel.GetSymbolInfo(invocationExpr).Symbol;
if (invokedSymbol == null) {
return; // it couldn't figure out what this symbol is (e.g. overload resolution failure) so we can't do anything with it
}
// ^ this is where the problem is
}