servicetitan / Stl.Fusion

Build real-time apps (Blazor included) with less than 1% of extra code responsible for real-time updates. Host 10-1000x faster APIs relying on transparent and nearly 100% consistent caching. We call it DREAM, or Distributed REActive Memoization, and it's here to turn real-time on!
MIT License
1.82k stars 106 forks source link

Assistance/Improvement for Factories/Proxies using ActivatorUtilities #649

Closed AliveDevil closed 8 months ago

AliveDevil commented 8 months ago

I'd like to know whether there is any better way of creating a Factory, that intercepts methods calls on that interface for passing these on to ActivatorUtilities, or if there is interest in this being integrated into Stl.Interception as a native supported pattern:

// should be singleton or scoped in MEDI
public sealed class ActivatorInterceptor(IServiceProvider services) : Interceptor
{
    private readonly ConcurrentDictionary<MethodInfo, ObjectFactory> _factories = [];
    private readonly IServiceProvider _services = services;

    public override TResult Intercept<TResult>(Invocation invocation)
    {
        var m = _factories.GetOrAdd(invocation.Method, CreateFactory<TResult>);
        return (TResult)m(_services, invocation.Arguments.ToArray());
    }

    private static ObjectFactory CreateFactory<T>(MethodInfo method) => CreateFactory<T>(method.GetParameters());

    private static ObjectFactory CreateFactory<T>(ParameterInfo[] parameters)
    {
        var arguments = new Type[parameters.Length];
        for (int i = 0; i < parameters.Length; i++)
        {
            arguments[i] = parameters[i].ParameterType;
        }

        return ActivatorUtilities.CreateFactory(typeof(T), arguments);
    }
}

public interface IFactory<TFactory> where TFactory : IFactory<TFactory>, IRequiresAsyncProxy;

Registering a factory:

public static IServiceCollection AddFactory<TFactory>(this IServiceCollection services) where TFactory : class, IFactory<TFactory>, IRequiresAsyncProxy
{
    services.AddSingleton(CreateFactory);
    return services;

    static TFactory CreateFactory(IServiceProvider services)
    {
        var interceptor = services.GetRequiredService<ActivatorInterceptor>();
        return services.ActivateProxy<TFactory>(interceptor);
    }
}

The interface is to make sure, that Stl.Generated-proxies are found correctly, and are available for consumption - this could probably be reduced to AddFactory<T> () where T : IRequiresAsyncProxy, but for me I implemented it this way for now.

And eventually could be used for something like:

public interface IViewModelFactory : IFactory<IViewModelFactory>, IRequiresFullProxy
{
    ConcreteViewModelTypeA ConcreteViewModelTypeA(int arg1, int arg2);
}

which returns an instance of

public record class ConcreteViewModelTypeA(
    int arg1,
    int arg2,
    ISomeService service,
    IOtherService otherService);

Regarding ArgumentList.ToArray() is probably not the best way to go about it (ObjectFactory is defined as delegate object? ObjectFactory(IServiceProvider, params object[] args), but I didn't see any other way as I figured ArgumentList.GetInvoker() isn't going to cut it, as it won't create the params-array when called.

alexyakunin commented 8 months ago

That's interesting. Am I right the proposal implies:

  1. Adding ~ IMethodBasedFactory - an interface you can extend
  2. Adding ~ IServiceCollection.AddMethodBasedFactory<TFactory>() where TFactory: IMethodBasedFactory, which resolves to a proxy acting as you've described. I.e. it automatically wires up any method call on it to an implementation, which create the actual object relying on ActivatorUtilities.CreateFactory & method call arguments.

And ultimately, it allows to replace untyped ~ services.Activate(...) calls with typed ~ myFactory.CreateSomething(...), + also boost the performance of such calls due to activator/factory caching?

alexyakunin commented 8 months ago

I'd also add proxy support for that, btw - i.e. auto-replacement of a type with Stl.Interception proxy, if it's available for that type (coz most likely that's the expected outcome in such cases).

alexyakunin commented 8 months ago

@AliveDevil I like the idea - it feels handy, + moreover, if the "default" construction helper won't fit some specific case, you can always add extension methods to the interface for custom construction.

alexyakunin commented 8 months ago

If you have some time, please send this PR - I'll definitely accept it, though might make some changes afterwards :)

And I'll be happy to do this anyway at some point (not sure when yet) - I like the proposal.

P.S. Great you dug pretty far into that - this part is almost undocumented.

AliveDevil commented 8 months ago

I like IMethodBasedFactory - sounds good. Adding MethodBasedActivatorUtilitiesInterceptor for this as the interception proxy (using the ActivatorUtilities factory).

Does Stl have any support of dynamic proxies (not generated through Stl.Generation)? Otherwise making IMethodBasedFactoryDefaultProxy could be a challenge (or I'm missing something here).

P.S. Great you dug pretty far into that

If there's no documentation, I'm really stubborn and dig into everything until I have a solution. Not the best habit in terms of time-efficiency, but for some personal project it works out fine.

AliveDevil commented 8 months ago

[…] boost the performance of such calls due to activator/factory caching?

I'd not want to vouch for that, though it makes it way more type-safe and improves discoverability - the factory caching does make it faster, as there is just the initial discovery phase of the constructor, then the factory delegate performs a regular constructor call.

The ObjectFactory itself is using params object[] internally, so whether that makes any difference I'm not sure about - as all value types are boxed again.

I mean, there is the option of porting back ActivatorUtilities^1 to not rely on object[] args, but that may overshoot the goal by miles.

AliveDevil commented 8 months ago

You are free to edit the PR, it’s „allow edit for maintainers“ enabled.