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(); + } + } +}