SignalR Implementation of Automatic Client Method Injection

2023年9月25日 2116点热度 3人点赞 1条评论
内容目录

C# Writing a SignalR Client Requires Manual Injection of Client Methods:

            connection.On<string, string>("ReceiveMessage", (user, message) =>
            {
                this.Dispatcher.Invoke(() =>
                {
                   var newMessage = $"{user}: {message}";
                   messagesList.Items.Add(newMessage);
                });
            });

This can be cumbersome, so we need to implement automatic scanning of instance methods to register them automatically.

Define a class to hold instance method information:

	public class RegisterMethod
	{
		public MethodInfo MethodInfo { get; init; }

		public bool IsAsync { get; init; }

		public Type[] Types { get; init; }
	}

Define an attribute to annotate instance methods that need binding:

	[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
	public class SignalRAttribute : Attribute
	{
		public string Name { get; private set; }
		public SignalRAttribute(string name)
		{
			Name = name;
		}
	}

Then implement scanning and automatic registration:

	public static class SignalRHelper<T> where T : class
	{
		private static Dictionary<string, RegisterMethod> MethodCache = new();
		static SignalRHelper()
		{
			var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			foreach (var item in methods)
			{
				var attr = item.GetCustomAttribute<SignalRAttribute>();
				if (attr == null) continue;
				MethodCache.Add(attr.Name, new RegisterMethod
				{
					MethodInfo = item,
					IsAsync = item.ReturnType == typeof(Task),
					Types = item.GetParameters().Select(x => x.ParameterType).ToArray()
				});
			}
		}

		/// <summary>
		/// Bind methods to HubConnection
		/// </summary>
		/// <param name="connection"></param>
		/// <param name="targetObj"></param>
		public static void Bind(HubConnection connection, T targetObj)
		{
			foreach (var item in MethodCache)
			{
				MethodInfo? onMethod;

				// Has generic parameters
				if (item.Value.Types.Length > 0)
				{
					var ms = typeof(HubConnectionExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public)
					   .Where(x => x.Name == "On" &&
					   x.GetGenericArguments().Length == item.Value.Types.Length &&
					   x.GetParameters().Length == 3
					   );

					// Asynchronous method (HubConnection, methodName, Func<...., Task>)
					if (item.Value.IsAsync)
					{
						onMethod = ms.FirstOrDefault(x => x.GetParameters()[2].ParameterType!.Name!.Contains("Func"));
					}
					// Asynchronous method (HubConnection, methodName, Action<....>)
					else
					{
						onMethod = ms.FirstOrDefault(x => x.GetParameters()[2].ParameterType!.Name!.Contains("Action"));
					}
					if (onMethod != null) onMethod = onMethod.MakeGenericMethod(item.Value.Types);
				}
				else
				{
					if (item.Value.IsAsync)
					{
						onMethod = typeof(HubConnectionExtensions).GetMethod("On", BindingFlags.Static | BindingFlags.Public, new Type[]
						{
							typeof(HubConnection),
							typeof(string),
							typeof(Func<Task>)
						});
					}
					else
					{
						onMethod = typeof(HubConnectionExtensions).GetMethod("On", BindingFlags.Static | BindingFlags.Public, new Type[]
						{
							typeof(HubConnection),
							typeof(string),
							typeof(Action)
						});
					}
				}

				if (onMethod == null) continue;
				var del = Delegate.CreateDelegate(onMethod.GetParameters()[2].ParameterType, targetObj, item.Value.MethodInfo);
				onMethod.Invoke(null, new object[]
				{
					connection,
					item.Key,
					del
				});
			}
		}
	}

Simply annotate instance methods:

		[SignalR("A")]
		public async Task A(string a, string b)
		{
			await Task.CompletedTask;
		}

Usage:

MyService a = .... 
SignalRHelper<MyService>.Bind(_connection, a);

痴者工良

高级程序员劝退师

文章评论