using MAX.Plugins; using System.Reflection; using MAX.Framework.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; if (args.ElementAtOrDefault(0) == "configuration") { ConfigurationChecks.Run(); return; } if (args.ElementAtOrDefault(0) == "hangfire") { HangfireChecks.Run(); return; } // Run each scenario in a fresh process: default ALC intentionally cannot unload plugins. var finderType = typeof(ServiceCollectionExtensions).Assembly.GetType("MAX.Framework.DependencyInjection.StoreAppTypeFinder", true)!; var finder = finderType.GetProperty("Current")!.GetValue(null); var method = finderType.GetMethod("GetAssemblies", new[] { typeof(string) })!; var mode = args.ElementAtOrDefault(1) ?? "plugins"; List assemblies; try { assemblies = (List)method.Invoke(finder, new object[] { args[0] == "-" ? null : args[0] })!; } catch (TargetInvocationException ex) when (mode == "failure" && ex.InnerException is InvalidOperationException) { Console.WriteLine("PASS expected loading failure: " + ex.InnerException.Message); return; } if (mode == "failure") throw new Exception("Expected loading failure"); var plugins = assemblies.Where(a => a.GetName().Name is "Store.CloudProduct" or "Store.ImageCache").ToArray(); if (mode == "empty") { if (plugins.Length != 0) throw new Exception("Unexpected plugins"); Console.WriteLine("PASS empty/missing directory"); return; } if (plugins.Length != (mode == "image" ? 1 : 2)) throw new Exception("Unexpected plugin count"); if (assemblies.Select(a => a.FullName).Distinct().Count() != assemblies.Count) throw new Exception("Duplicate assemblies"); var mvc = new ServiceCollection().AddControllers(); foreach (var plugin in plugins) mvc.AddApplicationPart(plugin); var feature = new ControllerFeature(); mvc.PartManager.PopulateFeature(feature); if (!feature.Controllers.Any(t => t.Name == "ImageCacheController")) throw new Exception("ImageCache controller missing"); if (mode != "image" && !feature.Controllers.Any(t => t.Name == "GoodsController")) throw new Exception("Goods controller missing"); if (mode is "image" or "plugins") { var host = Host.CreateApplicationBuilder(); var core = typeof(ServiceCollectionExtensions).GetMethod("AddStoreCore", BindingFlags.Static | BindingFlags.NonPublic)!; host.Configuration["Search:Url"] = "https://host.invalid"; host.Configuration["ImageCache:ConnectionString"] = "host-only"; core.Invoke(null, new object[] { host, plugins, new Dictionary() }); if (host.Services.Any(d => d.ServiceType == typeof(Hangfire.JobStorage))) throw new Exception("Plugins must not register shared Hangfire storage"); using var built = host.Build(); var image = built.Services.GetRequiredKeyedService("ImageCache"); if (image.Configuration["ImageCache:ConnectionString"] is null or "host-only") throw new Exception("Image config isolation failed"); if (image.Configuration["Search:Url"] != null) throw new Exception("Host settings leaked into ImageCache"); if (host.Configuration["ImageCache:ConnectionString"] != "host-only") throw new Exception("Plugin modified host configuration"); if (mode == "plugins") { var cloud = built.Services.GetRequiredKeyedService("CloudProduct"); if (cloud.Configuration["Search:Url"] is null or "https://host.invalid") throw new Exception("Cloud config isolation failed"); if (host.Configuration["Search:Url"] != "https://host.invalid") throw new Exception("Cloud modified host configuration"); foreach (var name in new[] { "OpenSearchService", "UserFavoritesIndexService", "RawDataStorage", "GoodsIndexJob", "GoodsViolationIndexJob", "GoodsService" }) { var type = plugins.SelectMany(p => p.GetTypes()).Single(t => t.Name == name); using var scope = built.Services.CreateScope(); scope.ServiceProvider.GetRequiredService(type); } } if (!host.Services.Any(d => d.ServiceType.Name == "ImageCacheService")) throw new Exception("Plugin DI registration missing"); if (!host.Services.Any(d => d.ServiceKey is string key && key == "ImageCacheConnection")) throw new Exception("Plugin database registration missing"); var registeredMvc = built.Services.GetRequiredService(); var registered = new ControllerFeature(); registeredMvc.PopulateFeature(registered); if (!registered.Controllers.Any(t => t.Name == "ImageCacheController")) throw new Exception("Core MVC registration missing"); } Console.WriteLine($"PASS plugins={plugins.Length}, controllers={feature.Controllers.Count}; dependencies/types loaded" + (mode is "image" or "plugins" ? "; isolated configuration/DI/plugin lifecycle/MVC registered" : ""));