diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs index 1c9f4b2dd8..d587d556d8 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,15 +100,24 @@ 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 (!implementationType.GetCustomAttributes(typeof(DisablePropertyInjectionAttribute), true).IsNullOrEmpty()) + { + return registrationBuilder; + } + + if (moduleContainer.Modules.Any(m => m.AllAssemblies.Contains(implementationType.Assembly))) { 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..11216e39ec 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,62 @@ 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)); + + // Only assemblies that directly reference Volo.Abp.Core can contain AbpModule subclasses. + // This skips framework/third-party assemblies (Microsoft.Extensions.*, etc.) cheaply. + var abpCoreAssemblyName = typeof(AbpModule).Assembly.GetName().Name; + + foreach (var assembly in nonModuleAssemblies) + { + if (!assembly.GetReferencedAssemblies().Any(r => r.Name == abpCoreAssemblyName)) + { + continue; + } + + Type[] types; + try + { + types = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(t => t != null).ToArray()!; + } + catch (Exception) + { + continue; + } + + 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/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs index 4ec967ed01..6002c005b7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs @@ -128,20 +128,24 @@ public abstract class AbpApplicationBase : IAbpApplication protected virtual void WriteInitLogs(IServiceProvider serviceProvider) { - var logger = serviceProvider.GetService>(); - if (logger == null) + var loggerFactory = serviceProvider.GetService(); + if (loggerFactory == null) { return; } - var initLogger = serviceProvider.GetRequiredService().Create(); + var initLoggerFactory = serviceProvider.GetRequiredService(); - foreach (var entry in initLogger.Entries) + foreach (var entry in initLoggerFactory.GetAllEntries()) { + var categoryName = string.IsNullOrEmpty(entry.CategoryName) + ? nameof(AbpApplicationBase) + : entry.CategoryName; + var logger = loggerFactory.CreateLogger(categoryName); logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, entry.Formatter); } - initLogger.Entries.Clear(); + initLoggerFactory.ClearAllEntries(); } protected virtual IReadOnlyList LoadModules(IServiceCollection services, AbpApplicationCreationOptions options) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs index ea17388b9f..05924395d6 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs @@ -5,6 +5,8 @@ namespace Volo.Abp.Logging; public class AbpInitLogEntry { + public string CategoryName { get; set; } = default!; + public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs index 98264aab74..9dcfb9dad6 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs @@ -17,6 +17,7 @@ public class DefaultInitLogger : IInitLogger { Entries.Add(new AbpInitLogEntry { + CategoryName = typeof(T).FullName!, LogLevel = logLevel, EventId = eventId, State = state!, diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs index 638888493d..aa52ed491f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs @@ -1,14 +1,34 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Volo.Abp.Logging; public class DefaultInitLoggerFactory : IInitLoggerFactory { private readonly Dictionary _cache = []; + private readonly List> _entryLists = []; public virtual IInitLogger Create() { - return (IInitLogger)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger()); + return (IInitLogger)_cache.GetOrAdd(typeof(T), () => + { + var logger = new DefaultInitLogger(); + _entryLists.Add(logger.Entries); + return logger; + }); + } + + public virtual List GetAllEntries() + { + return _entryLists.SelectMany(l => l).ToList(); + } + + public virtual void ClearAllEntries() + { + foreach (var list in _entryLists) + { + list.Clear(); + } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs index 611e9d7477..1f06aadc86 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs @@ -1,6 +1,12 @@ -namespace Volo.Abp.Logging; +using System.Collections.Generic; + +namespace Volo.Abp.Logging; public interface IInitLoggerFactory { IInitLogger Create(); + + List GetAllEntries(); + + void ClearAllEntries(); } 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..efd1a193e6 --- /dev/null +++ b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +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); + private List _initLogEntries = []; + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + protected override IServiceProvider CreateServiceProvider(IServiceCollection services) + { + var serviceProvider = base.CreateServiceProvider(services); + + // Capture init log entries after Autofac Populate/Register but before WriteInitLogs clears them. + _initLogEntries = serviceProvider.GetRequiredService().GetAllEntries(); + + return serviceProvider; + } + + [Fact] + public void Should_Warn_For_Orphaned_Abp_Modules() + { + _initLogEntries + .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() + { + _initLogEntries + .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. + context.Services.AddTransient(); + } + } +}