diff --git a/docs/en/Module-Development-Basics.md b/docs/en/Module-Development-Basics.md index dfce0bf4b2..1fdd09f8b2 100644 --- a/docs/en/Module-Development-Basics.md +++ b/docs/en/Module-Development-Basics.md @@ -147,7 +147,7 @@ Lastly, you can override ``OnApplicationShutdown`` method if you want to execute ## Module Dependencies -In a modular application, it's not unusual for one module to depend upon another module(s). An Abp module must declare ``[DependsOn]`` attribute if it does have a dependency upon another module, as shown below: +In a modular application, it's not unusual for one module to depend upon another module(s). An ABP module must declare a ``[DependsOn]`` attribute if it does have a dependency upon another module, as shown below: ````C# [DependsOn(typeof(AbpAspNetCoreMvcModule))] @@ -162,6 +162,27 @@ You can use multiple ``DependsOn`` attribute or pass multiple module types to a A depended module may depend on another module, but you only need to define your direct dependencies. ABP investigates the dependency graph for the application at startup and initializes/shutdowns modules in the correct order. +## Additional Module Assemblies + +ABP automatically registers all the services of your module to the [dependency injection](Dependency-Injection.md) system. It finds the service types by scanning types in the assembly that defines your module class. That assembly is considered as the main assembly of your module. + +Typically, every assembly contains a separate module class definition. Then modules depend on each other using the `DependsOn` attribute as explained in the previous section. However, in some rare cases, your module may consist of multiple assemblies, and only one of them defines a module class, and you want to make the other assemblies parts of your module. In that case, you can use the `AdditionalAssembly` attribute as shown below: + +````csharp +[DependsOn(...)] // Your module dependencies as you normally do +[AdditionalAssembly(typeof(BlogService))] // A type in the target assembly +public class BlogModule +{ + //... +} +```` + +In this example, we assume that the `BlogService` class is inside one assembly (`csproj`) and the `BlogModule` class is inside another assembly (`csproj`). With the `AdditionalAssembly` definition, ABP will load the assembly containing the `BlogService` class as a part of the blog module. + +Notice that `BlogService` is only an arbitrary selected type in the target assembly. It is just used to indicate the related assembly. You could use any type in the assembly. + +> WARNING: If you need to use the `AdditionalAssembly`, be sure that you don't design your system in a wrong way. With this example above, the `BlogService` class' assembly should normally have its own module class and the `BlogModule` should depend on it using the `DependsOn` attribute. Do not use the `AdditionalAssembly` attribute when you can already use the `DependsOn` attribute. + ## Framework Modules vs Application Modules There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose: diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index e22fef4236..0e93bb7ec6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -233,7 +233,7 @@ public class AbpAspNetCoreMvcModule : AbpModule .GetRequiredService() .Modules .Where(m => m.IsLoadedAsPlugIn) - .Select(m => m.Type.Assembly) + .SelectMany(m => m.AllAssemblies) .Distinct(); AddToApplicationParts(partManager, moduleAssemblies); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs index 9664a2005e..dc39ef5f53 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs @@ -125,7 +125,7 @@ public static class ApplicationPartSorter var moduleDependedAssemblies = moduleDescriptor .Dependencies - .Select(d => d.Assembly) + .SelectMany(d => d.AllAssemblies) .ToArray(); return partManager.ApplicationParts @@ -161,6 +161,6 @@ public static class ApplicationPartSorter { return moduleContainer .Modules - .FirstOrDefault(m => m.Assembly == assembly); + .FirstOrDefault(m => m.AllAssemblies.Contains(assembly)); } } diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs index 03084d8351..4ee9ba5916 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs @@ -69,7 +69,7 @@ public static class AbpRegistrationBuilderExtensions 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.Assembly == implementationType.Assembly) && + if (moduleContainer.Modules.Any(m => m.AllAssemblies.Contains(implementationType.Assembly)) && implementationType.GetCustomAttributes(typeof(DisablePropertyInjectionAttribute), true).IsNullOrEmpty()) { registrationBuilder = registrationBuilder.PropertiesAutowired(new AbpPropertySelector(false)); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs index 0ccff2db23..ec20467ac9 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs @@ -188,11 +188,13 @@ public abstract class AbpApplicationBase : IAbpApplication { if (!abpModule.SkipAutoServiceRegistration) { - var assembly = module.Type.Assembly; - if (!assemblies.Contains(assembly)) + foreach (var assembly in module.AllAssemblies) { - Services.AddAssembly(assembly); - assemblies.Add(assembly); + if (!assemblies.Contains(assembly)) + { + Services.AddAssembly(assembly); + assemblies.Add(assembly); + } } } } @@ -279,11 +281,13 @@ public abstract class AbpApplicationBase : IAbpApplication { if (!abpModule.SkipAutoServiceRegistration) { - var assembly = module.Type.Assembly; - if (!assemblies.Contains(assembly)) + foreach (var assembly in module.AllAssemblies) { - Services.AddAssembly(assembly); - assemblies.Add(assembly); + if (!assemblies.Contains(assembly)) + { + Services.AddAssembly(assembly); + assemblies.Add(assembly); + } } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleDescriptor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleDescriptor.cs index dcb9cfc817..d10e858377 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleDescriptor.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleDescriptor.cs @@ -11,6 +11,8 @@ public class AbpModuleDescriptor : IAbpModuleDescriptor public Type Type { get; } public Assembly Assembly { get; } + + public List AllAssemblies { get; } public IAbpModule Instance { get; } @@ -26,6 +28,7 @@ public class AbpModuleDescriptor : IAbpModuleDescriptor { Check.NotNull(type, nameof(type)); Check.NotNull(instance, nameof(instance)); + AbpModule.CheckAbpModuleType(type); if (!type.GetTypeInfo().IsAssignableFrom(instance.GetType())) { @@ -34,6 +37,7 @@ public class AbpModuleDescriptor : IAbpModuleDescriptor Type = type; Assembly = type.Assembly; + AllAssemblies = AbpModuleHelper.GetAllAssemblies(type); Instance = instance; IsLoadedAsPlugIn = isLoadedAsPlugIn; diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs index 697543409e..8336433994 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs @@ -36,6 +36,27 @@ internal static class AbpModuleHelper return dependencies; } + + public static List GetAllAssemblies(Type moduleType) + { + var assemblies = new List(); + + var additionalAssemblyDescriptors = moduleType + .GetCustomAttributes() + .OfType(); + + foreach (var descriptor in additionalAssemblyDescriptors) + { + foreach (var assembly in descriptor.GetAssemblies()) + { + assemblies.AddIfNotContains(assembly); + } + } + + assemblies.Add(moduleType.Assembly); + + return assemblies; + } private static void AddModuleAndDependenciesRecursively( List moduleTypes, diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AdditionalAssemblyAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AdditionalAssemblyAttribute.cs new file mode 100644 index 0000000000..ac4165f85e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AdditionalAssemblyAttribute.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Volo.Abp.Modularity; + +/// +/// Used to define additional assemblies for a module. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class AdditionalAssemblyAttribute : Attribute, IAdditionalModuleAssemblyProvider +{ + public Type[] TypesInAssemblies { get; } + + public AdditionalAssemblyAttribute(params Type[]? typesInAssemblies) + { + TypesInAssemblies = typesInAssemblies ?? Type.EmptyTypes; + } + + public virtual Assembly[] GetAssemblies() + { + return TypesInAssemblies.Select(t => t.Assembly).Distinct().ToArray(); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/DependsOnAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/DependsOnAttribute.cs index 89970859e8..8792d146d9 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/DependsOnAttribute.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/DependsOnAttribute.cs @@ -1,5 +1,4 @@ using System; -using JetBrains.Annotations; namespace Volo.Abp.Modularity; @@ -9,12 +8,11 @@ namespace Volo.Abp.Modularity; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class DependsOnAttribute : Attribute, IDependedTypesProvider { - [NotNull] public Type[] DependedTypes { get; } public DependsOnAttribute(params Type[]? dependedTypes) { - DependedTypes = dependedTypes ?? new Type[0]; + DependedTypes = dependedTypes ?? Type.EmptyTypes; } public virtual Type[] GetDependedTypes() diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAbpModuleDescriptor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAbpModuleDescriptor.cs index 8a4fb1e270..472009bfb3 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAbpModuleDescriptor.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAbpModuleDescriptor.cs @@ -6,13 +6,36 @@ namespace Volo.Abp.Modularity; public interface IAbpModuleDescriptor { + /// + /// Type of the module class. + /// Type Type { get; } + /// + /// Main assembly that defines the module . + /// Assembly Assembly { get; } + + /// + /// All the assemblies of the module. + /// Includes the main and other assemblies defined + /// on the module using the attribute. + /// + List AllAssemblies { get; } + /// + /// The instance of the module class (singleton). + /// IAbpModule Instance { get; } + /// + /// Is this module loaded as a plug-in? + /// bool IsLoadedAsPlugIn { get; } + /// + /// Modules on which this module depends on. + /// A module can depend on another module using the attribute. + /// IReadOnlyList Dependencies { get; } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAdditionalModuleAssemblyProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAdditionalModuleAssemblyProvider.cs new file mode 100644 index 0000000000..3a26d8e918 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Modularity/IAdditionalModuleAssemblyProvider.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Volo.Abp.Modularity; + +public interface IAdditionalModuleAssemblyProvider +{ + Assembly[] GetAssemblies(); +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/AssemblyFinder.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/AssemblyFinder.cs index 4e9a37db1a..602c577050 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/AssemblyFinder.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/AssemblyFinder.cs @@ -29,7 +29,7 @@ public class AssemblyFinder : IAssemblyFinder foreach (var module in _moduleContainer.Modules) { - assemblies.Add(module.Type.Assembly); + assemblies.AddRange(module.AllAssemblies); } return assemblies.Distinct().ToImmutableList(); diff --git a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/AbpHttpClientWebModule.cs b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/AbpHttpClientWebModule.cs index 5fcab7a7f1..655f96f068 100644 --- a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/AbpHttpClientWebModule.cs +++ b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/AbpHttpClientWebModule.cs @@ -31,7 +31,7 @@ public class AbpHttpClientWebModule : AbpModule .ServiceProvider .GetRequiredService() .Modules - .Select(m => m.Type.Assembly) + .SelectMany(m => m.AllAssemblies) .Where(a => a.GetTypes().Any(AbpHttpClientProxyHelper.IsClientProxyService)) .Distinct()) { diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs index eb15943670..d06025302d 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs @@ -21,9 +21,14 @@ public class ModuleLoader_Tests modules.Length.ShouldBe(2); modules[0].Type.ShouldBe(typeof(IndependentEmptyModule)); modules[1].Type.ShouldBe(typeof(MyStartupModule)); + modules[1].Assembly.ShouldBe(typeof(MyStartupModule).Assembly); + modules[1].AllAssemblies.Count.ShouldBe(2); + modules[1].AllAssemblies[0].ShouldBe(typeof(IAbpApplication).Assembly); + modules[1].AllAssemblies[1].ShouldBe(typeof(MyStartupModule).Assembly); } [DependsOn(typeof(IndependentEmptyModule))] + [AdditionalAssembly(typeof(IAbpApplication))] public class MyStartupModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs index 2ba02eee6b..f3086dcb8c 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Reflection/AssemblyFinder_Tests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using NSubstitute; using Shouldly; using Volo.Abp.Modularity; @@ -50,6 +51,7 @@ public class AssemblyFinder_Tests { var moduleDescriptor = Substitute.For(); moduleDescriptor.Type.Returns(moduleType); + moduleDescriptor.AllAssemblies.Returns(new List { moduleType.Assembly }); return moduleDescriptor; } }