После выгрузки плагина не освобождается dll из AssemblyLoadContext

За основу взят пример с github dotnet https://github.com/dotnet/samples/blob/main/core/tutorials/Unloading/Host/Program.cs, однако метод GetMessage() из PluginClass был переработан, возвращает Task<string>, имеет следующее тело:

using (var httpClient = new HttpClient())
{
    var response = await httpClient.GetAsync(new Uri("https://jsonplaceholder.typicode.com/posts"));
    var responseContent = await response.Content.ReadAsStringAsync();

    var doc = JsonDocument.Parse(responseContent);
    var models = doc.RootElement.EnumerateArray().Select(element => new Model
        {
            UserId = element.GetProperty("userId").GetInt32(), 
            Id = element.GetProperty("id").GetInt32(), 
            Title = element.GetProperty("title").GetString(), 
            Body = element.GetProperty("body").GetString()
        }).ToList();
}

return "Hello from the unloadable plugin";

Этот код прекрасно работает и Assembly выгружается после вызова Unload() у AssemblyLoadContext. Однако если использовать System.Text.Json или Newtonsoft.Json, то dll не собирается выгружаться из контекста, как долго я бы ни ждал (я пробовал вплоть до 100 секунд и это сильно запредельное время в моих условиях использования).

Пример проблемного кода с использование System.Text.Json ниже:

using (var httpClient = new HttpClient())
{
    var response = await httpClient.GetAsync(new Uri("https://jsonplaceholder.typicode.com/posts"));
    var responseContent = await response.Content.ReadAsStringAsync();
    var models = JsonSerializer.Deserialize<List<Model>>(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}

return "Hello from the unloadable plugin";

Пример проблемного кода с использование Newtonsoft.Json ниже:

using (var httpClient = new HttpClient())
{
    var response = await httpClient.GetAsync(new Uri("https://jsonplaceholder.typicode.com/posts"));
    var responseContent = await response.Content.ReadAsStringAsync();
    var models = JsonConvert.DeserializeObject<List<Model>>(responseContent);
}

return "Hello from the unloadable plugin";

Как считаете, с чем данное поведение связано и можно ли его каким-либо образом избежать?


Ответы (1 шт):

Автор решения: Георгий

Спустя некоторое время мне удалось разобраться в проблеме в контексте System.Text.Json (что в моём случае абсолютно устраивает меня).

Для рефлексии при работе с типами пользователя создаются долгоживущие экземпляры JsonSerializerOptions (скорее всего под этим подразумевается кэш опций, подробнее здесь).

Для решения данной проблемы предусмотрен метод ClearCache() в классе System.Text.Json.JsonSerializerOptionsUpdateHandler. Иначе говоря, в сборке Host (на основе примера из dotnet/samples из вопроса), где мы подключаем плагин, перед Unload() достаточно добавить следующее, и проблема уйдет:

var assembly = typeof(JsonSerializerOptions).Assembly;
var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler");
var clearCacheMethod = updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public);
clearCacheMethod?.Invoke(null, new object?[] { null });
→ Ссылка