Browse Source

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.
pull/25223/head
maliming 6 months ago
parent
commit
6ccb0ad689
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 20
      framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs
  2. 56
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs
  3. 58
      framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/WarnForOrphanedAbpModules_Tests.cs

20
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<Assembly>? 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<TLimit, TActivatorData, TRegistrationStyle> EnablePropertyInjection<TLimit, TActivatorData, TRegistrationStyle>(
this IRegistrationBuilder<TLimit, TActivatorData, TRegistrationStyle> registrationBuilder,
IModuleContainer moduleContainer,
Type implementationType)
Type implementationType,
HashSet<Assembly>? 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;

56
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<Assembly>();
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<Assembly> nonModuleAssemblies)
{
if (nonModuleAssemblies.Count == 0)
{
return;
}
var logger = services.GetInitLogger<AbpAutofacServiceProviderFactory>();
var loadedModuleTypes = new HashSet<Type>(
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.");
}
}
}
}
}

58
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<WarnForOrphanedAbpModules_Tests.TestModule>
{
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<IInitLoggerFactory>();
var logger = initLoggerFactory.Create<AbpAutofacServiceProviderFactory>();
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<IInitLoggerFactory>();
var logger = initLoggerFactory.Create<AbpAutofacServiceProviderFactory>();
// 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<AbpTestModule>();
}
}
}
Loading…
Cancel
Save