From 6ccb0ad689e5011d4dd1bd3275cd8bcb0fef2752 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 9 Apr 2026 12:56:42 +0800 Subject: [PATCH] Add warning for ABP modules not in the [DependsOn] chain When a type's assembly contains an ABP module but is not part of the module dependency chain, Autofac's property injection is silently skipped. This causes LazyServiceProvider and other injected properties to remain null, leading to NullReferenceException at runtime. This change detects the misconfiguration at startup and logs a warning with the affected assembly name, module type, and a fix suggestion. --- .../AbpRegistrationBuilderExtensions.cs | 20 +++++-- .../AutofacRegistration.cs | 56 +++++++++++++++++- .../WarnForOrphanedAbpModules_Tests.cs | 58 +++++++++++++++++++ 3 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs index 1c9f4b2dd8..db910d21e0 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs @@ -20,7 +20,8 @@ public static class AbpRegistrationBuilderExtensions ServiceDescriptor serviceDescriptor, IModuleContainer moduleContainer, ServiceRegistrationActionList registrationActionList, - ServiceActivatedActionList activatedActionList) + ServiceActivatedActionList activatedActionList, + HashSet? nonModuleAssemblies = null) where TActivatorData : ReflectionActivatorData { registrationBuilder = registrationBuilder.InvokeActivatedActions(activatedActionList, serviceDescriptor); @@ -37,7 +38,7 @@ public static class AbpRegistrationBuilderExtensions return registrationBuilder; } - registrationBuilder = registrationBuilder.EnablePropertyInjection(moduleContainer, implementationType); + registrationBuilder = registrationBuilder.EnablePropertyInjection(moduleContainer, implementationType, nonModuleAssemblies); registrationBuilder = registrationBuilder.InvokeRegistrationActions(registrationActionList, serviceType, implementationType, serviceDescriptor.ServiceKey); return registrationBuilder; @@ -99,14 +100,21 @@ public static class AbpRegistrationBuilderExtensions private static IRegistrationBuilder EnablePropertyInjection( this IRegistrationBuilder registrationBuilder, IModuleContainer moduleContainer, - Type implementationType) + Type implementationType, + HashSet? nonModuleAssemblies) where TActivatorData : ReflectionActivatorData { // Enable Property Injection only for types in an assembly containing an AbpModule and without a DisablePropertyInjection attribute on class or properties. - if (moduleContainer.Modules.Any(m => m.AllAssemblies.Contains(implementationType.Assembly)) && - implementationType.GetCustomAttributes(typeof(DisablePropertyInjectionAttribute), true).IsNullOrEmpty()) + if (moduleContainer.Modules.Any(m => m.AllAssemblies.Contains(implementationType.Assembly))) { - registrationBuilder = registrationBuilder.PropertiesAutowired(new AbpPropertySelector(false)); + if (implementationType.GetCustomAttributes(typeof(DisablePropertyInjectionAttribute), true).IsNullOrEmpty()) + { + registrationBuilder = registrationBuilder.PropertiesAutowired(new AbpPropertySelector(false)); + } + } + else + { + nonModuleAssemblies?.Add(implementationType.Assembly); } return registrationBuilder; diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs index b92182db92..642681db0c 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs @@ -24,7 +24,9 @@ // OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using Autofac.Builder; using Autofac.Core; @@ -33,7 +35,9 @@ using Autofac.Core.Activators.Delegate; using Autofac.Core.Activators.Reflection; using Autofac.Core.Resolving.Pipeline; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Volo.Abp; +using Volo.Abp.Autofac; using Volo.Abp.Modularity; namespace Autofac.Extensions.DependencyInjection; @@ -301,6 +305,10 @@ public static class AutofacRegistration var registrationActionList = services.GetRegistrationActionList(); var activatedActionList = services.GetServiceActivatedActionList(); + // Assemblies where property injection was skipped because they are not in the module chain. + // Collected in EnablePropertyInjection when a type registration's assembly is not part of any loaded module. + var nonModuleAssemblies = new HashSet(); + foreach (var descriptor in services) { var implementationType = descriptor.NormalizedImplementationType(); @@ -314,7 +322,7 @@ public static class AutofacRegistration .RegisterGeneric(implementationType) .ConfigureServiceType(descriptor) .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) - .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList); + .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList, nonModuleAssemblies); } else { @@ -322,7 +330,7 @@ public static class AutofacRegistration .RegisterType(implementationType) .ConfigureServiceType(descriptor) .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) - .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList); + .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList, nonModuleAssemblies); } continue; @@ -374,5 +382,49 @@ public static class AutofacRegistration .ConfigureLifecycle(descriptor.Lifetime, null) .ExternallyOwned(); } + + WarnForOrphanedAbpModules(services, moduleContainer, nonModuleAssemblies); + } + + private static void WarnForOrphanedAbpModules( + IServiceCollection services, + IModuleContainer moduleContainer, + HashSet nonModuleAssemblies) + { + if (nonModuleAssemblies.Count == 0) + { + return; + } + + var logger = services.GetInitLogger(); + + var loadedModuleTypes = new HashSet( + moduleContainer.Modules.Select(m => m.Type)); + + foreach (var assembly in nonModuleAssemblies) + { + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(t => t != null).ToArray()!; + } + + foreach (var type in types) + { + if (AbpModule.IsAbpModule(type) && !loadedModuleTypes.Contains(type)) + { + logger.LogWarning( + $"Assembly '{assembly.GetName().Name}' has services registered in the DI container, " + + $"but its ABP module '{type.FullName}' is not in the [DependsOn] chain. " + + "Property injection (e.g. LazyServiceProvider) will not work for these types " + + "and may cause NullReferenceException at runtime. " + + $"Add typeof({type.Name}) to your module's [DependsOn] attribute to fix this."); + } + } + } } } diff --git a/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs new file mode 100644 index 0000000000..1fcf04f9aa --- /dev/null +++ b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shouldly; +using Volo.Abp.Logging; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Autofac; + +public class WarnForOrphanedAbpModules_Tests : AbpIntegratedTest +{ + private static readonly Type OrphanModuleType = typeof(AbpTestModule); + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [Fact] + public void Should_Warn_For_Orphaned_Abp_Modules() + { + var initLoggerFactory = GetRequiredService(); + var logger = initLoggerFactory.Create(); + + logger.Entries + .Where(e => e.LogLevel == LogLevel.Warning) + .ShouldContain(e => e.Message.Contains(OrphanModuleType.FullName!), + $"Expected a warning for orphaned module '{OrphanModuleType.FullName}'."); + } + + [Fact] + public void Should_Not_Warn_For_Loaded_Modules() + { + var initLoggerFactory = GetRequiredService(); + var logger = initLoggerFactory.Create(); + + // AbpAutofacModule IS in the DependsOn chain, so it should NOT trigger a warning. + logger.Entries + .Where(e => e.LogLevel == LogLevel.Warning) + .ShouldNotContain(e => e.Message.Contains(typeof(AbpAutofacModule).FullName!), + "Modules in the [DependsOn] chain should not be reported as orphaned."); + } + + [DependsOn(typeof(AbpAutofacModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Simulate ASP.NET Core's AddControllersAsServices() registering a type + // from an assembly whose ABP module is NOT in the [DependsOn] chain. + // AbpTestModule lives in the Volo.Abp.Core.Tests assembly which is not depended on. + context.Services.AddTransient(); + } + } +}