0

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
}
ghosttie
  • 588
  • 2
  • 8
  • 18
  • Your question is a bit confusing: if the overload is unambiguous, we will return the single overload. The only case where you'll get multiple is the code is already ambiguous, but I don't understand why you'd care in your case. Either you wait until the user picks a specific overload and then you can error (because maybe not every overload has an async equivalent), or you just block all First methods in favor of FirstAsync and you don't worry about overloads. – Jason Malinowski Aug 22 '22 at 21:54
  • First, isn't there already [an analyzer](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1849) for this? Second: be careful about always recommending the use of Async methods: [there are some cases](https://stackoverflow.com/questions/33630317/very-slow-tolistasync-compared-to-tolist) where a function like `ToListAsync` takes far longer than `ToList`. – StriplingWarrior Aug 22 '22 at 23:15
  • @StriplingWarrior yes, but CA1849 only applies in an async method, I also want to catch cases where one of my developers has incorrectly called the sync version from a sync method, when they should have called the async version from an async method. And I'm using this to catch a specific case - database queries using EF Core, which I always want to be async. – ghosttie Aug 22 '22 at 23:29
  • @JasonMalinowski I've added some more detail - the method call isn't ambiguous. Is there any other data I can provide? – ghosttie Aug 22 '22 at 23:42
  • 1
    It might help to [see](https://github.com/dotnet/roslyn-analyzers/blob/main/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContext.cs) how CA1849 solves this problem. – StriplingWarrior Aug 23 '22 at 16:49
  • @StriplingWarrior CA1849 only applies in an async method, I also want to catch cases where one of my developers has incorrectly called the sync version from a sync method, when they should have called the async version from an async method. – ghosttie Aug 23 '22 at 19:09
  • 1
    Yes, I understood that when you explained it before. But one thing that's _not_ different is that they had to identify when certain methods are called, and that's the specific problem you're trying to solve (overload resolution, e.g.). It's probably worthwhile to look at their code and see how they solved that particular problem, even though the surrounding details are different. – StriplingWarrior Aug 23 '22 at 23:15
  • @ghosttie It feels like something else is off here then -- your analyzer is running in the context of code that is otherwise compiling successfully? Which node are you passing in to GetSymbolInfo? Note it's important to pass the entire invocation -- just the dotted name may give you the ambiguity as well. – Jason Malinowski Aug 26 '22 at 00:05
  • @JasonMalinowski I've added the code for the analyzer and the source I'm testing. The source I'm testing does compile in a standalone project, so I think it's OK. The node I'm passing to GetSymbolInfo is the context.Node tha gets passed into the analyzer. – ghosttie Aug 26 '22 at 15:59
  • 1
    @StriplingWarrior thank you, looking at the source for CA1849 was very educational (and I will steal some ideas from them), unfortunately they seem to just call GetSymbolInfo same as I do, so as JasonMalinowski says there must be something else more fundamentally wrong with my code – ghosttie Aug 28 '22 at 14:12

0 Answers0