-
Notifications
You must be signed in to change notification settings - Fork 29
Pure.DI and preserving module level encapsulation #34
-
Hello,
I am not sure how or is it possible to be able to register classes in the executing assembly container of different modules/assemblies without the need to make the types public.
Example:
Assembly A contains those types
public interface IMyInterface {}
internal class MyInterfaceImpl : IMyInterface { ... }
Executing Assembly need to register the dependency this way
partial class Composition
{
private static void Setup() =>
DI.Setup(nameof(Composition))
.Bind<IMyInterface >().To<MyInterfaceImpl>();
}
but it won't compile because MyInterfaceImpl is internal class
Is it possible to avoid making MyInterfaceImpl public and still be able to register the dependency?
We currently use Unity container and we do this by exposing extension method in Assembly A which register the dependencies on the container instance. The Executing assembly calls this method. This way we have encapsulation on module level but still be able to register dependencies.
Beta Was this translation helpful? Give feedback.
All reactions
In short, your proposed scenario does not fit the pure DI paradigm. There is no complexity to analyze all types, including those not visible in the project where DI is configured. But what to do with them next? Pure.DI generates code in the pure DI paradigm, which is just a set of nested constructors, for example:
partial class Composition { private object _lockObject = new object(); private Random _randomSingleton; public Program Root { get { Func<State> stateFunc = new Func<State>(() => { if (_randomSingleton == null) { lock (_lockObject) { if (_randomSingleton == null) ...
Replies: 4 comments 9 replies
-
The place in an application where you configure DI is usually the top-level module responsible for the composition of the application and providing an entry point into it. Mark Seemann in his book Dependency Injection has covered this issue in one of the chapters. The point is that this module can depend on all the modules of the application to be able to perform the application composition. In other words you can make all required internal types visible to this module where you configure DI. If you are using the modern .NET project model, then add the <InternalsVisibleTo Include="MyConsoleApp" /> element to all your projects whose internal types you want to make visible to "MyConsoleApp". For example, the project file where MyInterfaceImpl is located might look like this:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <InternalsVisibleTo Include="MyConsoleApp" /> </ItemGroup> </Project>
And the top module where you configure DI is called MyConsoleApp.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes, that is the theory. In practice we have more than 10 top-level projects that reference hundreds of shared assemblies which register their dependencies by themself. If I follow the recommendations of Mark Seemann, we will have to duplicate shared project registrations in every top level module and that is something I want to avoid even if it is the wrong way to do it. In practice the developer working in shared project must make dozens of commits to each top level module to register its shared dependency. That leads to other problems with git access to those modules and etc. For me the practical issues outweighs, benefits. Additionally we already adopted that aproach with Unity. We followed the approach that Microsoft used in ASP.NET Core each module to register itself. My question again is, does Pure.DI allows me to do that. Thanks.
Beta Was this translation helpful? Give feedback.
All reactions
-
My research found that Pure.DI alternatives like StrongInject and Jab provide concept of Module imports, which allows to achieve what I asked here. Please consider this as an options for Pure.DI
Beta Was this translation helpful? Give feedback.
All reactions
-
In short, your proposed scenario does not fit the pure DI paradigm. There is no complexity to analyze all types, including those not visible in the project where DI is configured. But what to do with them next? Pure.DI generates code in the pure DI paradigm, which is just a set of nested constructors, for example:
partial class Composition { private object _lockObject = new object(); private Random _randomSingleton; public Program Root { get { Func<State> stateFunc = new Func<State>(() => { if (_randomSingleton == null) { lock (_lockObject) { if (_randomSingleton == null) { _randomSingleton = new Random(); } } } return (State)_randomSingleton.Next(2); }); return new Program( new CardboardBox<ICat>( new ShroedingersCat( new Lazy<Sample.State>( stateFunc)))); } } public T Resolve<T>() { ... } public object Resolve(Type type) { ... } }
Beta Was this translation helpful? Give feedback.
All reactions
-
Alternatively, you can configure DI in each project and use only public types in the settings in top-level components.
Beta Was this translation helpful? Give feedback.
All reactions
-
@ekalchev Was my answer helpful? Feel free to ask if something is wrong.
Beta Was this translation helpful? Give feedback.
All reactions
-
Microsoft introduced Dependency Registrations abstractions through Microsoft.Extensions.DependencyInjection.Abstractions after the advent of .NET Core. Leveraging these abstractions, Microsoft has developed a series of NuGet packages that efficiently handle registrations using the provided abstractions. The genius of this approach lies in the fact that each of these NuGet packages exposes a class with extension methods for IServiceCollection, simplifying the service registration process. This solution is agnostic to the specific Dependency Injection (DI) framework in use, as long as it adheres to Microsoft.Extensions.DependencyInjection.Abstractions.
Our codebase, consisting of over 200 C# projects, is organized into multiple NuGet packages that our seven top-level projects utilize. In alignment with Microsoft's approach, we've adopted a strategy where each NuGet package can autonomously manage registrations by exposing an extension method for IServiceCollection. This extension method efficiently registers all services within the associated C# projects into the provided IServiceCollection container.
I realize this might be a significant change, but I'm excited about the potential of using something like Pure.DI in our codebase. Specifically, I'm curious about enabling registrations from different assemblies within our projects. It could make our project more modular and easier to maintain.
I'm not sure if Pure.DI already supports this. Do you think it's doable with your library, or would it need some modifications? Your insights would be greatly appreciated.
Beta Was this translation helpful? Give feedback.
All reactions
-
@ivan-prodanov Thank you for your interest in this project!
Pure.DI offers a different approach compared to traditional dependency injection containers. It is not a library, but a helper in writing code in the Pure.DI paradigm.
Imagine that you write all your code in the Pure.DI paradigm. You have no containers. A component called MyLib contains the IService interface and its MyService implementation. Another top-level component, such as the console application MyApp, contains a Consumer class uses IService in its logic, which is implemented to it via a constructor.
You have several options on how to do this, but in a pure DI paradigm, it is preferable to do it this way. Create an instance of the Consumer class with all the necessary dependencies in the MyApp component, like this:
var consumer = new Consumer(new MyService()); consumer.UseIt();
A very important point is that the dependency tree is defined at compile time and is completely static:
classDiagram
Consumer --> IService
and the composition of objects advising it is also defined at the compilation stage and transformed into code that uses new operators. This code is checked by the compiler and all optimizations are performed.
If the MyLib component contained some source or binary code for setting up the dependency tree, then before the "main" compilation it would be necessary to perform some "preliminary" compilation, run this code, get the metadata for building the dependency tree and run the main compilation with the created object compositions.
flowchart TD
Find[Find registration comonents]
--> |set of projects or libs| PreCompile[Preliminary compilation]
--> |assemblies| Run[Load assemblies and run registrations]
--> |metadata| Generation[Create a dependency tree and generate code]
--> |code of compositions| Compilation[Main compilation]
But this logic of "dynamic dependencies" can change the dependency tree, for example, depending on the environment parameters, as MS DI for ASP.NET Core depending on the application profile can use different endpoints or different loggers.
It is better to change your way of thinking here. And do not think of Pure.DI as a library of containers, but perceive it as a tool for generating source code of object compositions.
Or it can be used like this. MyLib contains a lot of interfaces and their implementations. MyApp depends on some very limited set of abstractions (1-10 interfaces for example). Pure.DI can help to write code to create instances of these abstractions with all their dependencies. They can already be registered in the ServiceCollection.
Beta Was this translation helpful? Give feedback.
All reactions
-
I believe he is not asking about dynamic dependency resolution. May be an example of an implementation for the discussed feature in another compile time DI container/source generator will bring come clarity on the topic.
More info about the module feature. By my humble understanding of compile time source generators, I believe there is no runtime dependency resolution here.
My understanding of this feature is that you can create multiple small parts of compile time registrations in each assembly and then 'glue' them together in the executing assembly. None of this involve runtime dependency resolution.
Beta Was this translation helpful? Give feedback.
All reactions
-
More info about the module feature. By my humble understanding of compile time source generators, I believe there is no runtime dependency resolution here.
Yes, I may have misunderstood the question. Does this approach work for different projects, different assemblies, or NuGet packages?
It could potentially be possible to use DependsOn for sub-projects. I'll try to play with it.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes it does. I ran some tests with StrongInject and it seems it does what I describe above. You can have AssemblyLibrary that have many internal classes. The AssemblyLibrary itself makes the compile time DI registrations to a IModule, mapping public interfaces to internal classes and then in the executable assembly merge this IModule to its registrations and was able to resolve types that are internal for AssemblyLibrary
There are three viable options for compile time DI in .net so far - StrongInject, Jab and Pure.DI. The other two libraries both support module feature. I already linked StrongInject module feature, here is Jab Modules. I would rather pick Pure.DI from the three but modules are must for us. It just not possible not to have this for big projects.
Correction - It seems the claims that StrongInject support internal class registrations are not true. I just tried what they said should work but it doesn't. Here it small sample what I am trying to achieve
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
I made an example solution that has both a library called Lib and an application that uses it App.
Now the App project references the Lib project as a dependency on the source code. In order for the source code dependency to work, the App project additionally contains a reference to the \Lib\Composition.cs file:
<ItemGroup> <ProjectReference Include="..\Lib\Lib.csproj"/> <Compile Include="..\Lib\Composition.cs"/>. <Link>Lib\Composition.cs</Link> </Compile> </ItemGroup>
If the Lib library is used as a binary dependency from the NuGet package, everything will work too. If you build a Lib package, it looks like this:
The top part will add the Composition.cs file to the project as Lib/Composition.cs and it will participate in the compilation to provide bindings metadata. And the binary file Lib.dll will be used as a normal library, it contains abstractions and implementations.
I use this approach in my projects.
I plan to add a project template to make it faster to create such libraries from scratch, I will add documentation about it.
As promised I will investigate the possibility of implementing your proposed feature.
Beta Was this translation helpful? Give feedback.
All reactions
-
Added template v 2.0.1 with template-short-name dilib. It can be used to create libraries using Pure.DI:
dotnet new -i Pure.DI.Templates dotnet new dilib
Beta Was this translation helpful? Give feedback.