Scrutor icon indicating copy to clipboard operation
Scrutor copied to clipboard

Native AoT compatibility ?

Open orosbogdan opened this issue 1 year ago • 12 comments

Is this library native aot compatible/ does it run well on ios ?

orosbogdan avatar Oct 11 '24 21:10 orosbogdan

Hey @orosbogdan! 👋🏻

I haven't tested it, but I have a feeling the answer is "no", since it does assembly scanning, relying heavily on reflection. I guess there's only one way to find out 😅

khellang avatar Oct 14 '24 07:10 khellang

Code generator

嘿 @orosbogdan! 👋🏻

我還沒有測試過它,但我感覺答案是“否”,因為它進行程序集掃描,嚴重依賴反射。我想只有一個方法可以找到答案😅

flier268 avatar Nov 08 '24 02:11 flier268

@khellang I thought about coming up with some code-gen for generating registrations at compile-time, so assembly scanning might shift left (to compile-/design-time).

I love how this lib eases up service registrations. That said, I recently saw other libs using code-gen to generate the needed boilerplate, but used some attribute-based approach, which tied life-time, etc. to implementations, which I really feel is wrong (like good old MEF did it back then). F. e. https://github.com/patrickklaeren/AutoRegisterInject

Scrutor seems to be the perfect fit to marry those two concerns (code-gen + expressive, well known "scanning-api" ).

This probably might play very well with AOT-compatibility (at least for registrations - not sure about deorations , atm).

Would you be interested in a design / draft PR for this?

earloc avatar Apr 21 '25 14:04 earloc

I'd be very happy if someone has the time and/or expertise to look into this. I'd rather agree on a design up-front than having a PR with a lot of work stall, like #129 👍

khellang avatar Apr 21 '25 14:04 khellang

Nice, @khellang! Got some hands on with source-generators lately over at https://github.com/earloc/TypealizR.

So my current "bad" gut feeling tells me, that doing assembly scanning within a source-generator might turn into a PITA. Or even that a final solution might need to "understand" the semantics of Scrutor enough, to "translate" the reflection-heavy stuff into their corresponding counterparts of roslyn (to not slow down builds, f.e.) - but at least it seems totally possible, to do so.

Would love to jump in an see, if this whole adventure might be feasible at all.

I just tried to sketch out something over at my fresh fork, based on the tests in the repo (see this commit).

In a nutshell, I imagine something like this (based on one of the tests):

  1. A (static?) "configuration"-method uses Scrutors fluent-API to express scanning rules, etc.
[Scrutor]
private void _UsingRegistrationStrategy_None(ITypeSourceSelector scan) => scan
    .FromAssemblyOf<ITransientService>()
        .AddClasses(classes => classes.AssignableTo<ITransientService>())
            .AsImplementedInterfaces()
            .WithTransientLifetime()
        .AddClasses(classes => classes.AssignableTo<ITransientService>())
            .AsImplementedInterfaces()
            .WithSingletonLifetime()
;
  1. a source-generator picks up this method, performs "some magic"™️, and generates something like this:

internal static partial class ServiceCollectionExtensions
{
    [GeneratedCode("Scrutor", "6.0.0")]
    internal static IServiceCollection ScanUsingRegistrationStrategy_None(this IServiceCollection services)
    {
        services.AddTransient<ITransientService, TransientService1>();
        services.AddTransient<ITransientService, TransientService2>();
        services.AddTransient<ITransientService, TransientService>();
        services.AddTransient<ITransientService, UnwantedNamespace.TransientService>();

        services.AddSingleton<ITransientService, TransientService1>();
        services.AddSingleton<ITransientService, TransientService2>();
        services.AddSingleton<ITransientService, TransientService>();
        services.AddSingleton<ITransientService, UnwantedNamespace.TransientService>();

        return services;
    }
}
  1. a dev happily uses the generated extension-method, which does not rely on reflection (at least not at runtime), anymore ;)
Collection.ScanUsingRegistrationStrategy_None();

Going forward, I'd like to explore how this could be done (f.e., coming up with a technical POC).

In the meantime, we'd have plenty of time thinking about "how to release this into the wild" (f.e. would this be better released as another(sibling-) nuget or not, etc. ?).

What else would you "need" for an upfront design? (Might make sense, to to turn this into a discussion (or reference a new one, here), instead of an issue?

earloc avatar Apr 21 '25 15:04 earloc

just had a glance over the changes in #129 and tbh, I don't get the concept behind it ;) (but also wouldn't bother to at least try, either :) )

earloc avatar Apr 21 '25 15:04 earloc

Thinking about this, it might also turn into a direction, where we would "just" need a roslyn-capable implementation of ITypeSourceSelector (and everything beneath), which would only need to surface the exact same behavior, like the current reflection-based implementations do, but acting in roslyn-land.

I guess this would be a perfect first POC: getting some user-code in place, which "configures" the behavior of a source-generator 🤔. Might as well try this out in my repo, first (as there is more infrastructure in place, already).

Stay tuned.

earloc avatar Apr 21 '25 16:04 earloc

Hey @khellang.

I think I managed to come up with a workable POC on how to use user-defined code to influence the behavior of a source-generator. In a nutshell: Aiming to execute csharp-script at compile-time, to "discover" any service-registrations, which normally would happen at run-time (Similar to what this article describes).

Details: see https://github.com/earloc/TypealizR/pull/265 Next step would be to see, if this approach could also be applied to Scrutor in such a way, which still supports all of the possibilities the fluent-API offers (which might reveal some unknown obstacles)...

Depending on the current test-coverage here, I would say this should be good enough, in order to make assumptions about overall compatibility with such an approach (if all tests are refatored like showcased here).

Image

One key-aspect, though, would still be to ensure types are discovered at compile-time ( in roslyn-land ) in the same manor, as ordinary reflection-based approach would do during app-bootstrap - which might turn out to be the most complex part of this attempt.

Did you have any chance to think about a possible release-strategy or any other topics? Personally, I feel like coming up with a dedicated Scrutor.Analyzers(like #129 proposed), might be the best streamlined way for evolution. Any additional thoughts highly appreciated...

Image

earloc avatar Apr 22 '25 19:04 earloc

I recently saw other libs using code-gen to generate the needed boilerplate, but used some attribute-based approach, which tied life-time, etc. to implementations, which I really feel is wrong

Alternatively, you can take a look at https://github.com/Dreamescaper/ServiceScan.SourceGenerator. While it's still attribute-based, it uses attributes to define type search criterias instead of binding to specific types.

Dreamescaper avatar Apr 23 '25 06:04 Dreamescaper

I recently saw other libs using code-gen to generate the needed boilerplate, but used some attribute-based approach, which tied life-time, etc. to implementations, which I really feel is wrong

Alternatively, you can take a look at https://github.com/Dreamescaper/ServiceScan.SourceGenerator. While it's still attribute-based, it uses attributes to define type search criterias instead of binding to specific types.

Hmmm, looks like a nice approach, as the attributes doesn't need to be tied to implementations. Just wondering, why you decided to come up with new package, instead of contributing here, f. e..

Especially, as the package clearly was "inspired by Scrutor", as stated in the docs.

earloc avatar Apr 23 '25 15:04 earloc

I felt like Scrutor's fluent API is very hard to get to work with source generators. Especially considering that there were abandoned attempts to do that.

Besides, I had no intention to implement other Scrutor's functionality, like Decorators.

Dreamescaper avatar Apr 23 '25 16:04 Dreamescaper

@khellang Just wanted to give an update (sorry for the spam ;)):

So I managed to get a somewhat working prototype together over the last days (which is in a really rough / early proof-of-concept stage, atm).

Main idea was, to leverage an adhoc CSharpScript to drive the discovery of registrations (which basically works). I'm using a simple ScriptContext to pass an instance of ITypeSourceSelector over to the script and later apply it to a IServiceCollection, from which the registered services are then extracted and populated in a generated extension-method.

example

UserCode

public interface IAbstraction
{
}

public class TransientImplementation : IAbstraction
{
}

public class ScopedImplementation : IAbstraction
{
}

public class SingletonImplementation : IAbstraction
{
}

public static class Config
{
    public static void Configure(ITypeSourceSelector Scan)
    {
        Scan.FromAssemblyOf<IAbstraction>()
            .AddClasses(classes => classes
                .AssignableTo<IAbstraction>()
                .Where(x => x.Name.StartsWith("Transient"))
            )
            .AsImplementedInterfaces()
            .WithTransientLifetime()
        ;

        Scan.FromAssemblyOf<IAbstraction>()
            .AddClasses(classes => classes
                .AssignableTo<IAbstraction>()
                .Where(x => x.Name.StartsWith("Scoped"))
            )
            .AsImplementedInterfaces()
            .WithScopedLifetime()
        ;

        Scan.FromAssemblyOf<IAbstraction>()
            .AddClasses(classes => classes
                .AssignableTo<IAbstraction>()
                .Where(x => x.Name.StartsWith("Singleton"))
            )
            .AsImplementedInterfaces()
            .WithSingletonLifetime()
        ;
    }
}

Generated code

namespace Microsoft.Extensions.DependencyInjection
    {
        public static class ScrutorServiceCollectionExtensions
        {
            public static IServiceCollection AddScannedServices(this IServiceCollection services)
            {
                services.AddTransient<Foo.IAbstraction, Foo.TransientImplementation>();
                services.AddScoped<Foo.IAbstraction, Foo.ScopedImplementation>();
                services.AddSingleton<Foo.IAbstraction, Foo.SingletonImplementation>();
                return services;
            }
        }
    }

This works in a unit-test.

Unfortunetely, there seems to be a big but(t), which seems to be rooted in how source-generators are executed in consuming projects.

When using this simple generator on an arbitrary project, script execution fails with something like:

[A]Scrutor.Analyzers.ScriptContext cannot be cast to [B]Scrutor.Analyzers.ScriptContext. Type A originates from 'Scrutor.Analyzers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context '"" Microsoft.CodeAnalysis.AnalyzerAssemblyLoader+DirectoryLoadContext #6' in a byte array. Type B originates from 'Scrutor.Analyzers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context '"Assembly.LoadFile(<PATHTOPROJECT>/Scrutor/src/Scrutor.Analyzers/bin/Debug/netstandard2.0/Scrutor.Analyzers.dll)" System.Runtime.Loader.IndividualAssemblyLoadContext #9' at location '<PATHTOPROJECT>/Scrutor/src/Scrutor.Analyzers/bin/Debug/netstandard2.0/Scrutor.Analyzers.dll'

Which is a clear sign of assemblies loaded twice.

Currently, I'm not 100% sure if this will be solvable (especially, as dealing with transitive dependencies in source-generators is quite a PITA, currently).

I'm not quite sure the current approach will stand ground - especially as a final solution still might need a custom implementations for ITypeSourceSelector, to not rely on reflection, but on roslyn`s complilation - which itself will be a challenge on it's own.

Nevertheless, a really great learning experience. And if the current approach turns out to be a dead-end, I also could imagine an alternative one (which would need a little more involvement from user-code side).

Basic concept: Scrutor only provides a source-generator-base, from which user-code could derive from (and specify all the rules to discover services).

earloc avatar Apr 29 '25 03:04 earloc

@khellang just a heads up on this.

I tinkered a bit more with this idea, but have to finally come to conclusion that this won't work the way I originally imagined.

For one thing, the whole "scripting" approach, where a source-generator would take in user-code, to execute it, suffers from really being executed in the compilation context of a project. THis might be solvable, but would need quite some stretches - and then there probably would wait a lot of unforseen edge-cases uncovered.

But the more important difficulty: Currently, source-generators - by design - are unaware of any other's source-generators output. See https://github.com/dotnet/roslyn/issues/57239

So even if the scipting-approach (or s.th. similar) would work like a charme, a source-generator just wouldn't be aware of every possible type in order to catch it up by Scrutors ITypeSourceSelector, as the final compilation may add types, etc., which would be needed to be discovered as well.

So in order to achieve this, a source-generator alone wouldn't be enough. Instead, some tooling (like a CLI / MSBuild-Task) would be needed, which is guaranteed to run after the final compilation is ready).

And having tools, which rely on source-code, generate source-code, which in turn then needs to be compiled again, might end up in difficult to resolve situations (BTDT;) - let alone, that in order to roundtrip it completely, at least 3 compilation passes are needed:

  1. initial compilation, letting every source-generator do it's thing
  2. additional pass via f.e. CLI, to run analysis and generate DI-registrations (as code)
  3. compilation again, to bake in the generated code from 2.

And now imagine, one refactors the codebase, breaking generated code form 2., so devs are no longer able to even do 1., without f.e. manually deleting / fixing stuff.

earloc avatar Jun 21 '25 10:06 earloc

I tinkered a bit more with this idea, but have to finally come to conclusion that this won't work the way I originally imagined.

Ouch. Yeah, source generators are quite limited (for good reason) and it seems like the fluent API of Scrutor would be a bit of a hassle to get working with source generators 😞

khellang avatar Jun 23 '25 12:06 khellang