dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.46k stars 10.03k forks source link

Client side hubs: Create a binder where you can pass a type and it will register methods as callbacks #5278

Closed moozzyk closed 3 years ago

moozzyk commented 7 years ago

The idea is to allow implementing a class that handles callbacks on the client side.

Server

public interface IStockTicker
{
    Task SendPrice(string symbol, decimal price);
}

public class TickerHub : Hub<IStockTicker>
{
     public Task UpdatePrice(string symbol, decimal price) => Clients.All.SendPrice(symbol, price);
}

Client

Traditional

public void Main()
{
      var hubConnection = new HubConnection();
      hubConnection.On<string, decimal>("SendPrice", (symbol, price) =>
      {
          Console.WriteLine($"Symbol: {symbol}, Price: {price}");
      });
     // Do the needful 
}

New

public class StockTicker : IStockTicker
{
   public  async Task SendPrice(string symbol, decimal price)
   {
        await Console.Out.WriteLineAsync($"Symbol: {symbol}, Price: {price}");
   }
}

public void Main()
{
      var hubConnection = new HubConnection<StockTicker>();
     // Do the needful 
}

Forked from #311

Open questions:

moozzyk commented 7 years ago

Related #290

muratg commented 7 years ago

Proxy generation work tracked in #290

davidfowl commented 7 years ago

Assigning to myself.

davidfowl commented 6 years ago

Cutting this feature for 2.1

davidfowl commented 6 years ago

Do we support dependency injection?

We did this! So it should be easy. The lifetime of those objects would still be in question though. On the client you'd want something long living (probably a singleton).

How do you handle running on the sync context etc?

We need to figure this out in general. We never did, even today...

The API to wire up callbacks needs work.

Yep.

muratg commented 5 years ago

@bradygaster cares about this. @davidfowl do you have thoughts for this for 3.0?

Misiu commented 5 years ago

Any chances this might get added to 3.0?

DoCode commented 5 years ago

@Misiu I have implemented a binder (in my situation a dispatcher) that works with IoC registered commands.

Misiu commented 5 years ago

@DoCode could You share Your solution or create a PR?

DoCode commented 5 years ago

@Misiu sorry, for a PR my time is currently very limited and the project itself is private, sorry! But I can share my solution here with a small description. It's very easy and works nicely in production.

Misiu commented 5 years ago

@DoCode that would be awesome :)

DoCode commented 5 years ago

SignalR client dispatcher

Attribute to explicite specify the class or method to dispatch

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
internal class EndPointAttribute : Attribute
{
    public EndPointAttribute(string name)
    {
        Name = name;
    }

    public string Name { get; }
}

You have a command or something else you would to execute when a endpoint is called from server

internal interface ICommand { }
[EndPoint("client-sleep-endpoint")]
internal class SleepCommand : ICommand
{
    // Methods with name 'ExecuteAsync' are automatically dispatched in classes marked with `EndPoint` attribute
    public Task ExecuteAsync(string someArgWhatInjectedDynamically)
    {
        return await Task.Delay(milliseconds).ConfigureAwait(false);
    }
}

Or use this syntax:

internal class SleepCommand : ICommand
{
    // Put this on methods then this dispatched
    [EndPoint("client-sleep-endpoint")]
    public Task SleepThenEat(string someArgWhatInjectedDynamically)
    {
        return await Task.Delay(milliseconds).ConfigureAwait(false);
    }

    // Then multiple endpoint in the same class allowed
    [EndPoint("client-sleep-longer")]
    public Task SleepThenGotoBed(string someArgWhatInjectedDynamically)
    {
        return await Task.Delay(milliseconds).ConfigureAwait(false);
    }
}

And here is the dispatcher

I removed logging and other things and commented out the factory methods, because I am working currently on factory dispatcher that get the commands per instance out of the IoC container. But this is WIP!

You must only call it with:

CommandDispatcher.BindCommands();
internal class CommandDispatcher
{
    private const BindingFlags DispatchBindingFlags = BindingFlags.IgnoreCase
                                                        | BindingFlags.IgnoreReturn
                                                        | BindingFlags.Instance
                                                        | BindingFlags.Public
                                                        | BindingFlags.NonPublic
                                                        | BindingFlags.Static;

    private const string DispatchMethodName = "ExecuteAsync";

    //private const string ServiceProviderFactoryName = "GetService";

    private readonly IEnumerable<ICommand> _commands;

    private readonly HubConnection _hubConnection;

    public CommandDispatcher(IEnumerable<ICommand> commands, HubConnection hubConnection)
    {
        _commands = commands;
        _hubConnection = hubConnection;

        //ServiceProvider provider = new ServiceCollection().BuildServiceProvider();
        //var lambda = new Action<object[]>(
        //    async args => await provider.GetService<RestartCommand>().ExecuteAsync((bool)args[0], (User)args[1]).ConfigureAwait(false));
    }

    private static Action<object[]> CreateAction(object instance, MethodInfo methodInfo)
    {
        ParameterExpression objectArrayParameter = Expression.Parameter(typeof(object[]), "args");

        ParameterInfo[] methodInfoParameters = methodInfo.GetParameters();
        var parameters = new Expression[methodInfoParameters.Length];
        for (var i = 0; i < methodInfoParameters.Length; ++i)
        {
            ConstantExpression index = Expression.Constant(i);
            Type methodInfoParameterType = methodInfoParameters[i].ParameterType;
            BinaryExpression objectArrayParameterAccessor = Expression.ArrayIndex(objectArrayParameter, index);
            UnaryExpression objectArrayParameterCast = Expression.Convert(objectArrayParameterAccessor, methodInfoParameterType);
            parameters[i] = objectArrayParameterCast;
        }

        MethodCallExpression call = Expression.Call(Expression.Constant(instance), methodInfo, parameters);
        Expression<Action<object[]>> lambda = Expression.Lambda<Action<object[]>>(call, objectArrayParameter);

        Action<object[]> compiled = lambda.Compile();
        return compiled;
    }

    //private static Action<object[]> CreateActionWithFactory(object instance, MethodInfo methodInfo, ServiceProvider serviceProvider)
    //{
    //    ConstantExpression serviceProviderConstant = Expression.Constant(serviceProvider);
    //    MethodInfo serviceProviderMemberInfo = typeof(ServiceProviderServiceExtensions).GetMethods(DispatchBindingFlags)
    //                                                                                    .SingleOrDefault(
    //                                                                                        x => x.Name == ServiceProviderFactoryName && x.IsGenericMethod)
    //                                                                                    ?.MakeGenericMethod(instance.GetType());
    //    if (serviceProviderMemberInfo == null) return null;

    //    MethodCallExpression serviceProviderCall = Expression.Call(null, serviceProviderMemberInfo, serviceProviderConstant);

    //    ParameterExpression objectArrayParameter = Expression.Parameter(typeof(object[]), "args");

    //    ParameterInfo[] methodInfoParameters = methodInfo.GetParameters();
    //    var parameters = new Expression[methodInfoParameters.Length];
    //    for (var i = 0; i < methodInfoParameters.Length; ++i)
    //    {
    //        ConstantExpression index = Expression.Constant(i);
    //        Type methodInfoParameterType = methodInfoParameters[i].ParameterType;
    //        BinaryExpression objectArrayParameterAccessor = Expression.ArrayIndex(objectArrayParameter, index);
    //        UnaryExpression objectArrayParameterCast = Expression.Convert(objectArrayParameterAccessor, methodInfoParameterType);
    //        parameters[i] = objectArrayParameterCast;
    //    }

    //    MethodCallExpression call = Expression.Call(serviceProviderCall, methodInfo, parameters);
    //    Expression<Action<object[]>> lambda = Expression.Lambda<Action<object[]>>(call, objectArrayParameter);

    //    Action<object[]> compiled = lambda.Compile();
    //    return compiled;
    //}

    private static IEnumerable<string> GetEndPointNames(MemberInfo memberInfo)
    {
        List<EndPointAttribute> endpointAttributes = memberInfo.GetCustomAttributes<EndPointAttribute>().ToList();
        return endpointAttributes.Select(x => x.Name);
    }

    private static bool HasEndPointNamesDefined(Type type)
    {
        // First search on class level
        List<EndPointAttribute> endpointAttributes = type.GetCustomAttributes<EndPointAttribute>().ToList();

        // Second on all methods
        MethodInfo[] methodInfos = type.GetMethods();
        foreach (MethodInfo methodInfo in methodInfos)
        {
            List<EndPointAttribute> methodEndPointAttributes = methodInfo.GetCustomAttributes<EndPointAttribute>().ToList();
            endpointAttributes.AddRange(methodEndPointAttributes);
        }

        return endpointAttributes.Count > 0 && endpointAttributes.Any(x => !string.IsNullOrEmpty(x.Name));
    }

    public void BindCommands()
    {
        IEnumerable<ICommand> commands = _commands.Where(x => HasEndPointNamesDefined(x.GetType()));

        foreach (ICommand command in commands)
        {
            var commandRegistered = false;

            // First search for default 'ExecuteAsync' method, for class level binding
            MethodInfo defaultMethodInfo = command.GetType().GetMethod(DispatchMethodName, DispatchBindingFlags);
            if (defaultMethodInfo != null)
            {
                ParameterInfo[] arguments = defaultMethodInfo.GetParameters();
                Type[] types = arguments.Select(x => x.ParameterType).ToArray();

                foreach (string endPointName in GetEndPointNames(command.GetType()))
                {
                    Action<object[]> action = CreateAction(command, defaultMethodInfo);

                    MethodInfo onMethod = GetType().GetMethod(nameof(On), DispatchBindingFlags);
                    if (onMethod != null)
                    {
                        onMethod.Invoke(this, new object[] { endPointName, types, action });
                        commandRegistered = true;
                    }
                }
            }

            // Second search on all methods
            MethodInfo[] methodInfos = command.GetType().GetMethods();
            foreach (MethodInfo methodInfo in methodInfos)
            {
                List<EndPointAttribute> methodEndPointAttributes = methodInfo.GetCustomAttributes<EndPointAttribute>().ToList();
                if (methodEndPointAttributes.Count <= 0) continue;

                foreach (string endPointName in GetEndPointNames(methodInfo))
                {
                    ParameterInfo[] arguments = methodInfo.GetParameters();
                    Type[] types = arguments.Select(x => x.ParameterType).ToArray();

                    Action<object[]> action = CreateAction(command, methodInfo);

                    MethodInfo onMethod = GetType().GetMethod(nameof(On), DispatchBindingFlags);
                    if (onMethod != null)
                    {
                        onMethod.Invoke(this, new object[] { endPointName, types, action });
                        commandRegistered = true;
                    }
                }
            }
        }
    }

    //public void BindCommandsWithFactory(ServiceProvider serviceProvider)
    //{
    //    Type commandInterfaceType = typeof(ICommand);
    //    IEnumerable<Type> availableCommandTypes = AppDomain.CurrentDomain.GetAssemblies()
    //                                                        .SelectMany(x => x.GetTypes())
    //                                                        .Where(x => commandInterfaceType.IsAssignableFrom(x) && x != commandInterfaceType);

    //    IEnumerable<Type> commandTypes = availableCommandTypes.Where(HasEndPointNamesDefined);

    //    foreach (Type commandType in commandTypes)
    //    {
    //        var commandRegistered = false;

    //        // First search for default 'ExecuteAsync' method, for class level binding
    //        MethodInfo defaultMethodInfo = commandType.GetMethod(DispatchMethodName, DispatchBindingFlags);
    //        if (defaultMethodInfo != null)
    //        {
    //            ParameterInfo[] arguments = defaultMethodInfo.GetParameters();
    //            Type[] types = arguments.Select(x => x.ParameterType).ToArray();

    //            foreach (string endPointName in GetEndPointNames(commandType))
    //            {
    //                Action<object[]> action = CreateActionWithFactory(commandType, defaultMethodInfo, serviceProvider);

    //                MethodInfo onMethod = GetType().GetMethod(nameof(On), DispatchBindingFlags);
    //                if (onMethod != null)
    //                {
    //                    onMethod.Invoke(this, new object[] { endPointName, types, action });
    //                    commandRegistered = true;
    //                }
    //            }
    //        }

    //        // Second search on all methods
    //        MethodInfo[] methodInfos = commandType.GetMethods();
    //        foreach (MethodInfo methodInfo in methodInfos)
    //        {
    //            List<EndPointAttribute> methodEndPointAttributes = methodInfo.GetCustomAttributes<EndPointAttribute>().ToList();
    //            if (methodEndPointAttributes.Count <= 0) continue;

    //            foreach (string endPointName in GetEndPointNames(methodInfo))
    //            {
    //                ParameterInfo[] arguments = methodInfo.GetParameters();
    //                Type[] types = arguments.Select(x => x.ParameterType).ToArray();

    //                Action<object[]> action = CreateActionWithFactory(commandType, methodInfo, serviceProvider);

    //                MethodInfo onMethod = GetType().GetMethod(nameof(On), DispatchBindingFlags);
    //                if (onMethod != null)
    //                {
    //                    onMethod.Invoke(this, new object[] { endPointName, types, action });
    //                    commandRegistered = true;
    //                }
    //            }
    //        }

    //        if (!commandRegistered) { }
    //    }
    //}

    private IDisposable On(string methodName, Type[] parameterTypes, Action<object[]> handler)
    {
        // From internal SignalR code: Microsoft.AspNetCore.SignalR.Client.HubConnectionExtensions.On
        return _hubConnection.Connection.On(methodName,
                                            parameterTypes,
                                            (parameters, state) =>
                                            {
                                                var currentHandler = (Action<object[]>)state;
                                                currentHandler(parameters);
                                                return Task.CompletedTask;
                                            },
                                            handler);
    }
}

NOTE: I removed some things here and this code is only for demonstration. It works perfect in productin. So when you need help please ping me!

eduherminio commented 5 years ago

Thanks a lot for that example, @DoCode, seems worth considering until feature is officially added!

davidfowl commented 3 years ago

Closing as dupe of https://github.com/dotnet/aspnetcore/issues/15198