Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Blazor for source generator to perform service injection instead of built-in reflection #54857

Closed
1 task done
sake402 opened this issue Mar 31, 2024 · 3 comments
Closed
1 task done
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved

Comments

@sake402
Copy link

sake402 commented Mar 31, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

No response

Describe the solution you'd like

We use blazor webassembly for an application and we are in the process of optimizing things, especially startup time (after resouces are downloaded).

Blazor typically uses reflection internally to supply properties to component from parent and also inject services.

We have used a source generator to generate an override for SetParametersAsync for all components in our application and this gives us a better startup time that Blazor would rather use to create reflection based PropertySetter for our components.

We have also implemented our own IComponentActivator and it uses a source generator to create new instance of components on request instead of the built--in Activator.CreateInstance.

But we want more. Blazor also uses reflection to supply services to component properties decorated with InjectAttribute and we have a source generator that is part of our component activator that does this now.

public delegate IComponent ComponentInstantiator(IServiceProvider provider);

//Generated by source generator
internal partial class ModuleComponentInstantiatorProvider : IModuleComponentInstantiatorProvider
{
    public IEnumerable<KeyValuePair<Type, ComponentInstantiator>> Instantiators
    {
        get
        {
            yield return new KeyValuePair<Type, ComponentInstantiator>(typeof(LivingThing.Core.Authentication.Client.Pages.App), (serviceProvider) => new LivingThing.Core.Authentication.Client.Pages.App()
            {
                AutenticationService = serviceProvider.GetRequiredService<LivingThing.Core.Frameworks.Common.Authentication.IAuthenticationService>(),
                Assets = serviceProvider.GetRequiredService<LivingThing.Core.Frameworks.Client.Head.IAssetsCollection>(),
                Navigation = serviceProvider.GetRequiredService<Microsoft.AspNetCore.Components.NavigationManager>(),
                UnitOfWork = serviceProvider.GetRequiredService<LivingThing.Core.Frameworks.Client.Interface.IClientUnitOfWork>()
            });
        }
    }
}

internal partial class ComponentActivator : IComponentActivator
    {
        IServiceProvider _serviceProvider;
        static Dictionary<Type, ComponentInstantiator> instantiators = new Dictionary<Type, ComponentInstantiator>(1024);
        public ComponentActivator(IServiceProvider provider, IEnumerable<IModuleComponentInstantiatorProvider> _instantiators)
        {
            _serviceProvider = provider;
            lock (instantiators)
            {
                //collect all assembly instantiators into this static dictionary once
                if (instantiators.Count == 0)
                {
                    foreach (var instantiator in _instantiators)
                    {
                        foreach (var i in instantiator.Instantiators)
                        {
                            instantiators.Add(i.Key, i.Value);
                        }
                    }
                }
            }
        }
        public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
        {
            if (instantiators.TryGetValue(componentType, out var instantiator))
            {
                return instantiator(_serviceProvider);
            }
            return (IComponent)Activator.CreateInstance(componentType)!;
        }
    }

Now that all our component services are supplied to it by its generated module instantiator, there seems to be no way to tell blazor runtime to skip Injecting the properties again via its built-in reflection scheme.

Looking at the ComponentFactory source code, we can see that the reflection work starts here with MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags) and futher inside PropertySetter.cs

private static Action<IServiceProvider, IComponent> CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type)
{
    // Do all the reflection up front
    List<(string name, Type propertyType, PropertySetter setter, object? serviceKey)>? injectables = null;
    foreach (var property in MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags))
    {
        var injectAttribute = property.GetCustomAttribute<InjectAttribute>();
        if (injectAttribute is null)
        {
            continue;
        }

        injectables ??= new();
        injectables.Add((property.Name, property.PropertyType, new PropertySetter(type, property), injectAttribute.Key));
    }

    if (injectables is null)
    {
        return static (_, _) => { };
    }

    return Initialize;

    // Return an action whose closure can write all the injected properties
    // without any further reflection calls (just typecasts)
    void Initialize(IServiceProvider serviceProvider, IComponent component)
    {
        foreach (var (propertyName, propertyType, setter, serviceKey) in injectables)
        {
            object? serviceInstance;

            if (serviceKey is not null)
            {
                if (serviceProvider is not IKeyedServiceProvider keyedServiceProvider)
                {
                    throw new InvalidOperationException($"Cannot provide a value for property " +
                        $"'{propertyName}' on type '{type.FullName}'. The service provider " +
                        $"does not implement '{nameof(IKeyedServiceProvider)}' and therefore " +
                        $"cannot provide keyed services.");
                }

                serviceInstance = keyedServiceProvider.GetKeyedService(propertyType, serviceKey)
                    ?? throw new InvalidOperationException($"Cannot provide a value for property " +
                    $"'{propertyName}' on type '{type.FullName}'. There is no " +
                    $"registered keyed service of type '{propertyType}' with key '{serviceKey}'.");
            }
            else
            {
                serviceInstance = serviceProvider.GetService(propertyType)
                    ?? throw new InvalidOperationException($"Cannot provide a value for property " +
                    $"'{propertyName}' on type '{type.FullName}'. There is no " +
                    $"registered service of type '{propertyType}'.");
            }

            setter.SetValue(component, serviceInstance);
        }
    }
}

Much like one can skip using reflection on SetParametersAsync by not calling the base.SetParametersAsync after manually setting properties from ParameterView, we need something like this for InjectedServices.

We've thought of creating our own attribute and use this instead of InjectAttribute, so our source generator can use that to perform the injection, this will eliminate creating instances of PropertySetter, but that wont eliminate the call to MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags) in CreatePropertyInjector

Additional context

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label Mar 31, 2024
@mkArtak
Copy link
Contributor

mkArtak commented Mar 31, 2024

Thanks for contacting us. We already track this ask as part of #29550

@sake402
Copy link
Author

sake402 commented Mar 31, 2024

Thanks for the feedback. Looking at #29550, it seems to focus on SetParametersAsync though. which one can already handle using source generator.

I am looking for a way to use source generator for injected services as well instead of reflection.

@javiercn
Copy link
Member

javiercn commented Apr 2, 2024

@sake402 we are using that issue to track these types of changes.

When we tackle SetParametersAsync we'll likely tackle other areas of the framework that benefit from this.

@javiercn javiercn added the ✔️ Resolution: Duplicate Resolved as a duplicate of another issue label Apr 2, 2024
@javiercn javiercn closed this as completed Apr 2, 2024
@github-actions github-actions bot locked and limited conversation to collaborators May 3, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components ✔️ Resolution: Duplicate Resolved as a duplicate of another issue Status: Resolved
Projects
None yet
Development

No branches or pull requests

3 participants