Search code examples
c#linqpad

Linqpad failing on tasks when loading three scripts


In Linqpad 7, I am developing a complete testing environment for our REST apis. I currently have three scripts, that constitutes the testing environment:

  1. core_rest_extensions.linq - This defines various extension methods for generating a fluent api for wiring up a REST client (HttpClient). This is a statement script.

  2. core_rest_auth.linq - Authentication for our REST services and a common RestClient class, that inherits from HtpClient. Configuration etc. Loads #1. This is a program script.

  3. extensions.linq - Extension methods for the RestClient for a specific set of REST calls. Loads #1. and #2. This is a program script.

Both #2 and #3 have a simple Main() method that I use to test the basic connection. This works fine in both scripts.

async Task Main()
{
    var apiKey = ""; // Any key token with access. 
    var ocpKey = ""; // OCP-Apim-Subscription-key.
    var baseUrl = ""; // The base url of the client.
    var client = await RestClient.CreateClient(apiKey, ocpKey, baseUrl);
}

A fourth script then contains all the concrete the test cases for a specific API. This loads the 3 other scripts, in the mentioned order. It is a program script.

The exact same Main() in this script fails with an exception:
InvalidOperationException: A task may only be disposed if it is in a completion state (RanToCompletion, Faulted or Canceled).
This occurs in an extension method called PostAsync, which is defined in the first script.

The RestClient class is defined like this, in the core_rest_auth script:

public partial class RestClient : HttpClient
{
    public static async Task<RestClient> CreateClient(string apiKey, string ocpKey, string baseUrl)
    {
        var client = (RestClient)new RestClient() {
            BaseAddress = new Uri(baseUrl),
            Timeout = TimeSpan.FromMinutes(20)
        };
        client.DefaultRequestHeaders.Add("Accept", "application/json; charset=utf-8");
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", ocpKey);
        var auth = await client.AuthenticationLogin(new LoginDTO { ApiToken = apiKey });
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.Token);

        return client;
    }

    public async Task<TokenDTO> AuthenticationLogin(LoginDTO dto)
        => await this.PostAsync<TokenDTO>("authentication/login", (r) => r.AsJson(dto));
}

public class LoginDTO
{
    public string? ApiToken { get; set; }
}

public class TokenDTO
{
    public string? Token { get; set; }
}

The pertinent methods in the core_rest_extensions scripts are here:

public static async Task<T> PostAsync<T>(this HttpClient client, string path, Action<HttpRequestMessage>? requestPre = null) where T : class
{
    using (var req = path.Uri().Post())
    {
         requestPre?.Invoke(req);
        using (var res = client.SendAsync(req))
        {
            return await res.ValidateSuccess().ReadAsAsync<T>();
        }
   }
}

public static Uri Uri(this string text) => new Uri(text, UriKind.RelativeOrAbsolute);

public static HttpRequestMessage Post(this Uri uri) => new HttpRequestMessage(HttpMethod.Post, uri);

public static async Task<HttpResponseMessage> ValidateSuccess(this Task<HttpResponseMessage> msg)
{
    var m = await msg;
    if (m.IsSuccessStatusCode) return m;

    string result = null;
    try
    {
        result = await m.Content.ReadAsStringAsync();
    }
    catch { }
    throw new HttpResponseInfoException(m, result);
}

public static async Task<T?> ReadAsAsync<T>(this Task<HttpResponseMessage> msg) where T : class
{
    var m = await msg;
    if (m.Content.Headers.ContentType.MediaType.EndsWith("/json", StringComparison.CurrentCultureIgnoreCase))
    {
        try
        {
            return await m.Content.ReadFromJsonAsync<T>();
        }
        catch (Exception ex)
        {
            string content = await m.Content.ReadAsStringAsync();
            throw new DeserializeErrorException(content, ex);
        }
    }
}

public static HttpRequestMessage AsJson(this HttpRequestMessage msg, object content)
{
    if (content == null)
        return msg;

    var opts = new JsonSerializerOptions(JsonSerializerDefaults.Web) { IgnoreNullValues = true };
    var json = JsonSerializer.SerializeToUtf8Bytes(content, content.GetType(), opts);

    msg.Content = new ByteArrayContent(json);
    msg.Content.Headers.ContentLength = json.Length;
    msg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

    return msg;
}

As I mentioned, the exact same Main() definition in script #2 and #3 works fine. In script #4, it fails with the exception above. The exception occurs in the ValidateSuccess extension method.

I am not sure how to interpret this, but it feels like Linqpad is unable to keep tasks running correctly when more than two scripts are loaded into another.

1 #load - Fine.
2 #loads - Fine.
3 #loads - Task is disposed before it runs to completion.

Anyone have any insights into why this may be?


Solution

  • Ok, I found the issue. I started reducing extension methods, until I got another error. Specifically "Method not found" exception for the HttpContentJsonExtensions.ReadFromJsonAsync method from System.Net.Http.Json.

    I then started comparing namespaces and references between the scripts, and lo and behold. The script that failed has some extra references the others didn't.

    I am guessing one of these referenced an older version of System.Net.Http.Json which does not have this specific method.

    Once I removed the extra references, I had no errors.