Closed moozzyk closed 3 years ago
Related #290
Proxy generation work tracked in #290
Assigning to myself.
Cutting this feature for 2.1
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.
@bradygaster cares about this. @davidfowl do you have thoughts for this for 3.0?
Any chances this might get added to 3.0?
@Misiu I have implemented a binder (in my situation a dispatcher) that works with IoC registered commands.
@DoCode could You share Your solution or create a PR?
@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.
@DoCode that would be awesome :)
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
internal class EndPointAttribute : Attribute
{
public EndPointAttribute(string name)
{
Name = name;
}
public string Name { get; }
}
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);
}
}
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!
Thanks a lot for that example, @DoCode, seems worth considering until feature is officially added!
Closing as dupe of https://github.com/dotnet/aspnetcore/issues/15198
The idea is to allow implementing a class that handles callbacks on the client side.
Server
Client
Traditional
New
Forked from #311
Open questions: