I have an object - a client that connects to the server through a socket connection. This means that when I need to get something from the server, I send him a request packet, and he, when he likes it, will answer me. Accordingly, the entire logic of the answers is tied to the events - that is, at the moment when I receive the answer, I have no idea which of the requests this answer gave me.
I had an idea that this is not very good, and it would be much more structured to translate this model into a request-response model. For the experiment, I prepared a base class
public class Foo { public event EventHandler<FooEArgs> SomeEvent; public void SomeEventInvocator() { SomeEvent?.Invoke(this, new FooEArgs()); } public Action<Foo> SomeDelegate; public void SomeDelegateInvocator() { SomeDelegate?.Invoke(this); } public Task SendEvent() { return Task.Run(async () => { await Task.Delay(2000); SomeEventInvocator(); }); } public Task SendDelegate() { return Task.Run(async () => { await Task.Delay(2000); SomeDelegateInvocator(); }); } public class FooEArgs : EventArgs { } } Yes, events can be in the form of events, or they can be just delegates. Do not ask - this is the inherited code of the 3-party library. I just have to live with it.
Next, I wrote methods for waiting an event and a delegate
public Task WaitForDelegate(Foo foo) { var tcs = new TaskCompletionSource<int>(); Action<Foo> handler = null; handler = (s) => { foo.SomeDelegate -= handler; tcs.SetResult(0); }; foo.SomeDelegate += handler; return tcs.Task; } public Task WaitForEvent(Foo foo) { var tcs = new TaskCompletionSource<int>(); EventHandler<Foo.FooEArgs> handler = null; handler = (s, e) => { foo.SomeEvent-=handler; tcs.SetResult(0); }; foo.SomeEvent +=handler; return tcs.Task; } They seem to work, but they are strictly typed. Then I tried to write the Generic method at least for the event
private async Task WaitForEvent<T>(Action<EventHandler<T>> addEvent, Action<EventHandler<T>> removeEvent, TimeSpan timeout, EventHandler<T> customHandler = null) { var tcs = new TaskCompletionSource<int>(); EventHandler<T> handler = null; handler = (s, e) => { removeEvent(handler); customHandler?.Invoke(s, e); tcs.SetResult(0); }; addEvent(handler); await Task.WhenAny(tcs.Task, Task.Delay(timeout)); } Testing methods:
async Task Main() { var foo = new Foo(); foo.SendEvent(); Console.WriteLine("Waiting for event."); await WaitForEvent(foo); Console.WriteLine("Event raised."); foo.SendDelegate(); Console.WriteLine("Waiting for delegate."); await WaitForDelegate(foo); Console.WriteLine("Delegate raised."); foo.SendEvent(); Console.WriteLine("Waiting for event."); await WaitForEvent<Foo.FooEArgs>(h => foo.SomeEvent += h, h => foo.SomeEvent -= h, TimeSpan.FromMinutes(1), (s, e) => {Console.WriteLine("Inside event!");}); Console.WriteLine("Event raised."); } Output of the result:
Waiting for event. Event raised. Waiting for delegate. Delegate raised. Waiting for event. Inside event! Event raised. This method works, but the question remains - is this generally acceptable? Can this code be rewritten to more stable / understandable? Also, the code also has disadvantages, for example, for a good request to the server should go after the subscription, and not before the subscription to the event.
Of course, I read How to: Wrap EAP Patterns in a Task , however, my case is different in that I have a situation where many threads simultaneously send the same and / or different requests to the server, and then a bundle of identical and / or different events - in fact, I hope that in the new implementation I can also cache tasks in order to unload the server from such things. Also events can come by themselves, without any requests from the client.
Thus, I need to review my code, and perhaps the community will help me improve it or, on the contrary, abandon this idea.
UPD
As a result, such a class was born
public static class EventAwaiter { public static IEventAwaiter<T> FromEvent<T>(Action<EventHandler<T>> addEvent, Action<EventHandler<T>> removeEvent) { return new EventAwaiterInternal<T>(addEvent, removeEvent); } public static IEventAwaiter<T> WithTimeout<T>(this IEventAwaiter<T> target, TimeSpan timeout) { return target.SetTimeout(timeout); } public static IEventAwaiter<T> WithHandler<T>(this IEventAwaiter<T> target, EventHandler<T> handler) { return target.SetCustomHandler(handler); } public interface IEventAwaiter<T> { IEventAwaiter<T> SetTimeout(TimeSpan tieout); IEventAwaiter<T> SetCustomHandler(EventHandler<T> handler); Task Wait(Action raiseEvent = null, Func<object, T, bool> matchEvent = null); } private class EventAwaiterInternal<T> : IEventAwaiter<T> { private readonly Action<EventHandler<T>> _addEvent; private readonly Action<EventHandler<T>> _removeEvent; private TimeSpan _timeout = TimeSpan.MaxValue; private EventHandler<T> _customHandler; public EventAwaiterInternal(Action<EventHandler<T>> addEvent, Action<EventHandler<T>> removeEvent) { _addEvent = addEvent; _removeEvent = removeEvent; } public IEventAwaiter<T> SetTimeout(TimeSpan timeout) { _timeout = timeout; return this; } public IEventAwaiter<T> SetCustomHandler(EventHandler<T> handler) { _customHandler = handler; return this; } public async Task Wait(Action raiseEvent = null, Func<object, T, bool> matchEvent = null) { var tcs = new TaskCompletionSource<int>(); EventHandler<T> handler = null; handler = (s, e) => { if (matchEvent != null && !matchEvent(s, e)) return; _removeEvent(handler); _customHandler?.Invoke(s, e); tcs.SetResult(0); }; _addEvent(handler); raiseEvent?.Invoke(); await Task.WhenAny(tcs.Task, Task.Delay(_timeout)); _removeEvent(handler); } } } Calling it looks like this
var foo = new Foo(); await EventAwaiter.FromEvent<Foo.FooEArgs>(h=>foo.SomeEvent+=h, h=>foo.SomeEvent-=h) .WithTimeout(TimeSpan.FromMinutes(1)) .WithHandler((s, e) => Console.WriteLine("Inside!")) .Wait(()=>foo.SendEvent()); For me, the record was pretty short.