In Linqpad 7, I am developing a complete testing environment for our REST apis. I currently have three scripts, that constitutes the testing environment:
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.
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.
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?
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.