Search code examples
c#.net-coreserilog

Is there a way to disable or analyze Serilog parameter rendering of Task<T>?


I've recently refactored a method that retrieves a username from a dictionary of connected users to be asynchronous:

public static string GetUsername(this Guid id) => ...
public static async Task<string> GetUsernameAsync(this Guid id) => await ...

I expected a large refactor with Serilog usage, as I have hundreds of places that use this logging like so:

Log.Debug("User {UserName} disconnected from {IP}, saving", guid.GetUsername(), IP);

I renamed the method GetUsername to GetUsernameAsync, expecting hundreds of warnings that I could simply go through and refactor to "awaits" (both the serilog calls and the containing methods), but no such warnings appeared. I was expecting it to Warn, so that I could refactor the calls to something like:

Log.Debug("User {UserName} disconnected from {IP}, saving", await guid.GetUsernameAsync(), IP);

But alas, Serilog dutifully renders those "strings" as "Task strings" and grants me messages (synchronously) like so:

[08:57:09 DBG] System.Threading.Tasks.Task`1[System.String] disconnected from 127.0.0.1:64022, saving

Is there a way to do this refactor easily? Some configuration setting of Serilog or Analyzer that warns on Task parameters?


Solution

  • Here are a few ideas:

    1. If you want to add await for all (or even most) instances of the refactored method, you could use global find and replace in the IDE.
      1. For Visual Studio you can access this via the menu Edit > Find and Replace > Replace in Files (default keyboard shortcut is Ctrl+Shift+H) and use these settings:
        • Search string: (?:await\s+)?\b(\w+\.GetUsernameAsync)
        • Replace string: await $1
        • NOTE: The regex matches (but doesn't capture) an optional leading await so that if you've partially done the refactor by updating some usages you won't end up with await await in those places. It uses a word boundary (\b) and word characters (\w) so that it works with any variable name wherever GetUsernameAsync may be called.
      2. You should now have compiler errors for usages of await from non-async methods, which you can go through and fix manually.
      3. If needed, you can manually remove the await for specific usages of GetUsernameAsync where you don't want to immediately await the method (I'm guessing this is likely a rare or non-existent case).
    2. To review the results of your refactor or just use a more 1-by-1 manual approach instead of global find and replace, you can use Visual Studio Find All References (right click context menu for the method) and then enter log in the "Search Find All References" textbox in upper right of the tool window to focus on logging-related usages.
    3. You could look at implementing your own Roslyn code analyzer based on existing examples. While this may not be trivial (weigh the development and testing effort vs. the time to do a very large number of manual updates to await the refactored method) or be the easiest option for the refactor, it covers the "is there an analyzer?" part of your question. The Microsoft.VisualStudio.Threading project includes a number of analyzers and is open source. Particularly the analyzer with ID VSTHRD110 (source), which recommends observing the result of async calls (from non-async methods), may be a useful starting point. Out of the box, I don't think this analyzer would issue a warning for your specific use case because the return value from your async version of GetUsernameAsync is passed as an argument to a Log method. Hence the result is not entirely unused. However, you may be able to adjust it to create a new analyzer for your use case.