Dependency Injection for Native AOT

Andrii Kurdiumov
5 min readJun 17, 2022
Inject some objects into your code. Photo by FRANK MERIÑO.

Dependency injection in recent years is a very important topic, and Microsoft.Extensions.DependencyInjection package is second after Newtonsoft.Json in popularity. So what state of DI in Native AOT?

DI working in Native AOT

Obviously it is working because it only relies on reflection and M.E.DI (shorthand for Microsoft.Extensions.DependencyInjection) are properly annotated for trimming. Autofac working, DryIoc working, Grace working too. LightInject guess what? Working too. What about StrongInject? Working! Other DI containers should work too.

So why bother then and try to write an article about it? Because in addition to regular Native AOT there is a variant of it which completely disables reflection and can make your application even slimmer. So called reflection-free mode. Which is not supported by MS by the way. So please do not go to the runtime repo and do not file issues there regarding this usage scenario. Most likely they would be redirected to this issue. Only difference between supported and not supported is whether you have <IlcDisableReflection>true</IlcDisableReflection> in your project file, or not.

All aforementioned DI containers do not work in the reflection-free mode.

Let me explain what’s not working in so-called reflection-free mode so you can guess why DI can rely on these features.

Maybe you want to create an instance of type using a constructor?

var constructors = typeof(Test).GetConstructors();
var instance1 = constructors.First().Invoke(new Type[0]);

Or maybe you want to use non-generic Activator.CreateInstance ?

var instance2 = Activator.CreateInstance(typeof(Test));

All of that works in regular Native AOT, because it has reflection. But snippets above would not work in reflection-free mode, since you need to have some form of reflection metadata kept around. So what to do if you want to make your app leaner?

Reflection-free problems

You can think about DI as a glorified map between types and objects with some object instantiation sprinkled in. Types as keys are fine, but what’s in the keys is a potential problem.

In the M.E.DI you have following common ways to define your services:

  • Provide type implementation using AddXXX<TService, TImplementation>()
  • Provide factory method for service AddXXX<TService>(Func<IServiceProvider, object>
  • Provide object instance using AddXXX<TService>
  • Provide open-generic service declaration by declaring service and implementation using TService<X> and TImplementation<X> generic types.
  • IEnumerable usage of service, does not registered implicitly, but services of this kind created on request.

Providing factory methods and instances would work for the DI even in reflection-free, since that’s your code responsible for creation and I bet you do everything possible to make your code work in reflection-free. Other ways not so much. For example when you provide type implementation, then DI container somehow should create a service instance for you, and without completely disabled reflection it is not possible to do. Same for open generics.

So what to do?

My answer to this is source generator. I know, there at least 2 additional source generators which provide DI which works in reflection-free mode — Jab and Pure DI. What’s the difference? For me the only difference is that you can take existing code and make it run without rewriting for new DI. So instead of reinventing the wheel, I think that existing solutions should be improved to make it reflection-free friendly. So while writing this I realize that I can provide a solution for Autofac as well. Hopefully I have enough vigor to finish that project.

So my idea has two sides. First side is to provide a way to augment existing code in such a way that your code instantly becomes reflection-free friendly. Second side is to create a statically discovered container and make it as fast as handwritten code.

So I manage to make it both ways somewhat. I cannot say that it’s a success, but I think that’s interesting enough to share with the world.

Consider this sample code

I will provide 2 factory methods for each service registration using type. And the most important trick, I provide custom AddScoped implementation within the namespace of the class where AddScoped is used, so Rolsyn picks up my implementation instead of the extension method which comes from M.E.DI. Hopefully AddXXX methods are extension methods, so I can do my dirty tricks using source generators.

To illustrate generated code:

After the source generator is added to the project, all calls to AddXXX methods are reflection-free compatible. Limitations of that approach is that each assembly should have reference to this source generator. Otherwise you may have parts of your container which do not play well with reflection-free mode.

Second approach is more interesting, but more limiting. Simply by changing a call from BuildServiceProvider to BuildServiceProviderAot developer receives a container which has all registered services in the assembly, and can resolve it. Building of such containers happens extremely fast, as well as service resolution.

You can take a look at numbers, to compare.

Legend

MEDI — regular Microsoft Extensions Dependency Injection

MEDI_Augmented — MEDI container with source generator replacing calls to AddXXX

MEDI_Aot — Source generated container with services statically resolved at compile time. Similar to Jab and Pure.DI

Jab — is Jab :)

PureDI — is Pure.DI

I would not bet my money on the correctness of these numbers, and you can take a look at source code. At least I use the same approach as Jab.

Grain of salt

Open generics still does not work. So registrations like AddScoped<ILogger<>, Logger<>> cannot be used. Reason for that is that I do not see all registrations of ILogger<MyClass> during the call to AddScoped for example. I may scan for ILogger<MyClass> usages in the other services analyzed assembly, but that’s not enough to prove that I support open-generics. There is more work needed in that direction.

That affects the ability of this library to augment M.E.Logging and MediatR for example. And usage of M.E.Logging is huge in the .NET ecosystem.

Summary

̶C̶u̶r̶r̶e̶n̶t̶l̶y̶ ̶o̶n̶l̶y̶ ̶M̶E̶D̶I̶ ̶s̶u̶p̶p̶o̶r̶t̶s̶ ̶r̶e̶f̶l̶e̶c̶t̶i̶o̶n̶-̶f̶r̶e̶e̶ ̶m̶o̶d̶e̶. Right now only MEDI and other source generator based libraries like Jab, Pure.DI and StrongInject working with reflection-free mode, but that very likely other DI libraries can catch up if they really want this. I hope they do. Reflection-free mode is a good way to find unexpected usage of reflection.

My approach shows that MEDI out of the box supports reflection-free mode, if you use only factory or instance registrations. So you may not even need this source generator if you are willing to sacrifice some developer experience and do not use handy extension methods which breaks in reflection-free mode.

Also would be good if somebody gives practical feedback on the approach, and if there is interest in pursuing reflection-free mode even more.

Source code

Nuget

Sample

Very small sample, so you can just compile and try it yourself.

--

--

Andrii Kurdiumov

Math lover, lost in the woods of software development