ASP.NET Core 8.0添加AddApplicationPart与EmbeddedFileProvider后内存泄漏
核心问题分析
你添加EmbeddedFileProvider到Razor运行时编译选项后,每次测试初始化新服务容器时,该文件提供者关联的程序集及Razor编译缓存可能未被正确释放。Razor运行时编译会维护编译结果的缓存,若每次测试都新建EmbeddedFileProvider实例并关联同一程序集,再加上测试上下文的服务容器未完全清理,就会导致编译相关实例持续堆积,最终引发内存暴涨。
排查与解决建议
复用EmbeddedFileProvider实例
不要在每次服务配置时新建EmbeddedFileProvider,改为静态单例避免重复注册:private static readonly EmbeddedFileProvider _embeddedFileProvider = new EmbeddedFileProvider(typeof(HomeController).Assembly); // 测试服务配置中使用 services.AddControllersWithViews() .AddApplicationPart(typeof(HomeController).Assembly) .AddRazorRuntimeCompilation(options => { // 避免重复添加同一文件提供者 if (!options.FileProviders.Any(fp => fp == _embeddedFileProvider)) { options.FileProviders.Add(_embeddedFileProvider); } });重复添加会导致Razor编译系统多次处理同一程序集的视图资源,生成多份编译缓存,额外占用内存。
禁用测试环境的Razor编译缓存
测试场景下无需保留编译缓存,可通过代码或配置关闭:.AddRazorRuntimeCompilation(options => { options.FileProviders.Add(_embeddedFileProvider); // 设置缓存大小为0,禁用编译结果缓存 options.CacheSizeLimit = 0; })也可在
appsettings.Test.json中添加配置:{ "Razor": { "RuntimeCompilation": { "CacheSizeLimit": 0 } } }这样编译相关实例不会被缓存持有,更易被GC回收。
确保测试服务主机完全释放
检查测试基类是否正确实现IDisposable,在测试结束时主动停止并释放服务主机:public class TestBase : IDisposable { private IHost _host; protected IHost Host => _host ??= CreateHost(); private IHost CreateHost() { return Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .Build(); } public void Dispose() { _host?.StopAsync().Wait(); _host?.Dispose(); _host = null; } }必须调用
StopAsync()和Dispose(),确保Razor编译相关的后台服务被正确停止释放。检查嵌入式资源配置
确认模块项目中视图文件的「生成操作」设为「嵌入式资源」,且无重复嵌入同一视图的情况。重复的嵌入式资源会让Razor编译系统处理冗余资源项,增加内存占用。定位缓存持有源
使用Visual Studio内存探查器对比测试前后的内存快照,筛选RazorCompilation相关类型(如RazorCompiledItem、CompilerCache),查看引用链,找到持有这些实例不释放的对象——通常是测试服务容器中的单例服务或静态缓存。尝试共享服务容器(谨慎使用)
若所有测试依赖相同模块程序集,可通过xUnit的ICollectionFixture在集合级别初始化一次服务容器,避免每个测试重复创建:public class SharedServiceFixture : IDisposable { public IHost Host { get; } public SharedServiceFixture() { Host = Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .Build(); Host.StartAsync().Wait(); } public void Dispose() { Host.StopAsync().Wait(); Host.Dispose(); } } [CollectionDefinition("SharedServiceCollection")] public class SharedServiceCollection : ICollectionFixture<SharedServiceFixture> { } [Collection("SharedServiceCollection")] public class MyTests { private readonly SharedServiceFixture _fixture; public MyTests(SharedServiceFixture fixture) { _fixture = fixture; } }注意:此方式会共享服务状态,可能导致测试间污染,仅适合无状态API测试。
内容的提问来源于stack exchange,提问作者Erik Steinebach

