Search code examples
.netasp.net-coreblazorautofacblazor-webassembly

How to apply AOP logging on Blazor component lifecycle method with Autofac.Extras.DynamicProxy?


I want to solve cross cutting concerns with Blazor WebAssembly. I want to log all lifecycle methods call on a specific component like below.

<p>@nameof(BasicChild)</p>
<button @onclick="OnClick">StateHasChanged</button>

@code {
    public BasicChild()
    {
        Console.WriteLine("BasicChild()");
    }

    private int _initializedCount = 0;
    protected override void OnInitialized()
    {
        ++_initializedCount;
        var message = $"[{_initializedCount}]BasicChild.OnInitialized()";
        Console.WriteLine(message);
        base.OnInitialized();
    }

    private int _parametersSetCount = 0;
    protected override void OnParametersSet()
    {
        ++_parametersSetCount;
        var message = $"[{_parametersSetCount}]BasicChild.OnParametersSet()";
        Console.WriteLine(message);
        base.OnParametersSet();
    }

    private int _shouldRenderCount = 0;
    protected override bool ShouldRender()
    {
        ++_shouldRenderCount;
        var message = $"[{_shouldRenderCount}]BasicChild.ShouldRender()";
        Console.WriteLine(message);
        return base.ShouldRender();
    }

    private int _afterRenderCount = 0;
    protected override void OnAfterRender(bool firstRender)
    {
        ++_afterRenderCount;
        var message = $"[{_afterRenderCount}]BasicChild.OnAfterRender(firstRender: {firstRender})";
        Console.WriteLine(message);
        base.OnAfterRender(firstRender);
    }

    private void OnClick()
    {
        var message = $"BasicChild.StateHasChanged()";
        Console.WriteLine(message);
        StateHasChanged();
    }
}

I want to hide all logging code with Autofac.Extras.DynamicProxy. But I am not sure how to register a Blazor component to Container and use proxy class instead actual class when BuildRenderTree(RenderTreeBuilder) method generated.

I know I can override BuildRenderTree(RenderTreeBuilder) and resolve proxy component from container. But I want to stay on Blazor syntax.

Is there a way to do this?

What I desired result is like below.

<p>@nameof(BasicChild)</p>
<button @onclick="OnClick">StateHasChanged</button>
public class Program
{
    private static void ConfigureContainer(ContainerBuilder builder)
    {
        builder.RegisterType<BasicChild>()
            .As<IComponentLifecycle>()
            // ...
            .EnableInterfaceInterceptors()
            .InterceptedBy(typeof(LoggingInterceptor));
    }
}

Github aspnetcore issue


2021.10.16 Updates

according to @javiercn,

If you are trying to inject dependencies into a component you can implement your own IComponentFactory and use your container inside to resolve dependencies if needed. If you are trying to render a component at runtime, you can use DynamicComponent.


Solution

  • I solved this!

    public class ComponentActivator : IComponentActivator
    {
        private readonly ILogger<ComponentActivator> _logger;
        private readonly IComponentFactory _componentFactory;
    
        public ComponentActivator(ILogger<ComponentActivator> logger, [NotNull] IComponentFactory componentFactory)
        {
            _logger = logger;
            _componentFactory = componentFactory ?? throw new ArgumentNullException(paramName: nameof(componentFactory));
        }
    
        public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
        {
            if (!typeof(IComponent).IsAssignableFrom(componentType))
                throw new ArgumentException($"{componentType.FullName} 타입은 {nameof(IComponent)} 인터페이스를 구현하지 않아, RenderTree 에 추가할 인스턴스를 만들 수 없습니다.", nameof(componentType));
    
            if(_componentFactory.CanCreate(componentType))
                return _componentFactory.Create(componentType);
    
            return (IComponent)Activator.CreateInstance(componentType)!;
        }
    }
    
    public interface IComponentFactory
    {
        bool CanCreate(Type componentType);
        IComponent Create(Type componentType);
    }
    
    public class ComponentFactory : IComponentFactory
    {
        private readonly ILogger<ComponentFactory> _logger;
        private readonly ILifetimeScope _container;
    
        public ComponentFactory(ILogger<ComponentFactory> logger, ILifetimeScope container)
        {
            _logger = logger;
            _container = container;
        }
    
        public bool CanCreate(Type componentType) => _container.TryResolve(componentType, out _);
    
        public IComponent Create(Type componentType)
        {
            var component = _container.Resolve(componentType);
    
            if (component is null)
                throw new InvalidOperationException($"{_container.GetType()} 으로 {componentType.FullName} 인스턴스를 만들지 못했습니다.");
            else if (component is not IComponent)
                throw new InvalidOperationException($"'{component.GetType()}' 은 {nameof(IComponent)} 인터페이스를 구현하지 않았습니다.");
    
            return (IComponent)component;
        }
    }
    
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
    
            // https://autofac.readthedocs.io/en/latest/integration/blazor.html
            builder.ConfigureContainer(new AutofacServiceProviderFactory(ConfigureContainer));
            builder.Services.AddSingleton<IComponentFactory, ComponentFactory>();
            builder.Services.AddSingleton<IComponentActivator, ComponentActivator>();
    
            builder.RootComponents.Add<App>("#app");
    
            await builder.Build().RunAsync();
        }
    
        private static void ConfigureContainer(ContainerBuilder builder)
        {
            Castle.DynamicProxy.Generators.AttributesToAvoidReplicating.Add(typeof(System.Runtime.CompilerServices.AsyncStateMachineAttribute));
    
            builder.RegisterType<LoggingInterceptor>();
            
            builder
                .RegisterType<Account>()
                .EnableClassInterceptors()
                .InterceptedBy(typeof(LoggingInterceptor));
        }
    }