Search code examples
c#taskcontinuations

Complete a task when an event fires


This should be simple but I just can't bring it into focus.

In this method

public static async Task<string> UnloadAsync(Assembly assy, bool silentFail = false)
{
  if (AssyLoadContext.__alcd.ContainsKey(assy))
  {
    var assemblyName = __namd.Where(kvp => kvp.Value == assy).First().Key;
    __alcd[assy].Unloading += alc => //signal the task to complete and return assemblyName
    __namd.Remove(assemblyName);
    __alcd[assy].Unload();
    __alcd.Remove(assy);
    Debug.WriteLine($"Unloaded assembly '{assy.GetName().Name}'");
  }
  if (silentFail) 
  {
    return null;
  }
  else
  {
    throw new InvalidOperationException($"Assembly '{assy.GetName().Name}' cannot be unloaded. Did you load it using AssyLoadContext.LoadWithPrivateContext(string assyPath)?");
  }
}

the AssemblyLoadContext.Unload() operation is actually asynchronous, but isn't awaitable. Once there are no more strong GC references etc the operation completes, the assembly unloads and the Unloading event fires.

The return value is in assemblyName which I want to provide to a continuation.

All the documentation I can find blathers about the need for await because that's where the yield happens, but I can't write it that way. How do you do this without await?


Solution

  • You're looking for a TaskCompletionSource<string>:

    public static Task<string> UnloadAsync(Assembly assy, bool silentFail = false)
    {
      if (AssyLoadContext.__alcd.ContainsKey(assy))
      {
        var tcs = new TaskCompletionSource<string>();
        var assemblyName = __namd.Where(kvp => kvp.Value == assy).First().Key;
        __alcd[assy].Unloading += alc => tcs.SetResult(assemblyName);
        __namd.Remove(assemblyName);
        __alcd[assy].Unload();
        __alcd.Remove(assy);
        Debug.WriteLine($"Unloaded assembly '{assy.GetName().Name}'");
        return tcs.Task;
      }
      if (silentFail)
      {
        return Task.FromResult<string>(null);
      }
    
      throw new InvalidOperationException($"Assembly '{assy.GetName().Name}' cannot be unloaded. Did you load it using AssyLoadContext.LoadWithPrivateContext(string assyPath)?");
      }
    }
    

    Note that if this throws an InvalidOperationException, it gets thrown when UnloadAsync is called, rather than being wrapped in the returned Task (which is what would happen if your method was async). If you want to change this, you can either use the TaskCompletionSource:

    public static Task<string> UnloadAsync(Assembly assy, bool silentFail = false)
    {
      var tcs = new TaskCompletionSource<string>();
      if (AssyLoadContext.__alcd.ContainsKey(assy))
      {
        var assemblyName = __namd.Where(kvp => kvp.Value == assy).First().Key;
        __alcd[assy].Unloading += alc => tcs.SetResult(assemblyName);
        __namd.Remove(assemblyName);
        __alcd[assy].Unload();
        __alcd.Remove(assy);
        Debug.WriteLine($"Unloaded assembly '{assy.GetName().Name}'");
      }
      else if (silentFail)
      {
        tcs.SetResult(null);
      }
      else
      {
        tcs.SetException(new InvalidOperationException($"Assembly '{assy.GetName().Name}' cannot be unloaded. Did you load it using AssyLoadContext.LoadWithPrivateContext(string assyPath)?"));
      }
      
      return tcs.Task;
    }
    

    Or use an async method:

    public static async Task<string> UnloadAsync(Assembly assy, bool silentFail = false)
    {
      if (AssyLoadContext.__alcd.ContainsKey(assy))
      {
        var tcs = new TaskCompletionSource<string>();
        var assemblyName = __namd.Where(kvp => kvp.Value == assy).First().Key;
        __alcd[assy].Unloading += alc => tcs.SetResult(assemblyName);
        __namd.Remove(assemblyName);
        __alcd[assy].Unload();
        __alcd.Remove(assy);
        Debug.WriteLine($"Unloaded assembly '{assy.GetName().Name}'");
        return await tcs.Task;
      }
      if (silentFail)
      {
        return null;
      }
    
      throw new InvalidOperationException($"Assembly '{assy.GetName().Name}' cannot be unloaded. Did you load it using AssyLoadContext.LoadWithPrivateContext(string assyPath)?");
    }