using System; using System.Collections.Concurrent; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; namespace Umbraco.Core.Events { /// /// There is actually no way to discover an event name in c# at the time of raising the event. It is possible /// to get the event name from the handler that is being executed based on the event being raised, however that is not /// what we want in this case. We need to find the event name before it is being raised - you would think that it's possible /// with reflection or anything but that is not the case, the delegate that defines an event has no info attached to it, it /// is literally just an event. /// /// So what this does is take the sender and event args objects, looks up all public/static events on the sender that have /// a generic event handler with generic arguments (but only) one, then we match the type of event arguments with the ones /// being passed in. As it turns out, in our services this will work for the majority of our events! In some cases it may not /// work and we'll have to supply a string but hopefully this saves a bit of magic strings. /// /// We can also write tests to validate these are all working correctly for all services. /// internal class EventNameExtractor { /// /// Finds the event name on the sender that matches the args type /// /// /// /// /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched /// /// /// null if not found or an ambiguous match /// public static Attempt FindEvent(Type senderType, Type argsType, Func exclude) { var found = MatchedEventNames.GetOrAdd(new Tuple(senderType, argsType), tuple => { var events = CandidateEvents.GetOrAdd(senderType, t => { return t.GetEvents(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) //we can only look for events handlers with generic types because that is the only // way that we can try to find a matching event based on the arg type passed in .Where(x => x.EventHandlerType.IsGenericType) .Select(x => new EventInfoArgs(x, x.EventHandlerType.GetGenericArguments())) //we are only looking for event handlers that have more than one generic argument .Where(x => { if (x.GenericArgs.Length == 1) return true; //special case for our own TypedEventHandler if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2) { return true; } return false; }) .ToArray(); }); return events.Where(x => { if (x.GenericArgs.Length == 1 && x.GenericArgs[0] == tuple.Item2) return true; //special case for our own TypedEventHandler if (x.EventInfo.EventHandlerType.GetGenericTypeDefinition() == typeof(TypedEventHandler<,>) && x.GenericArgs.Length == 2 && x.GenericArgs[1] == tuple.Item2) { return true; } return false; }).Select(x => x.EventInfo.Name).ToArray(); }); var filtered = found.Where(x => exclude(x) == false).ToArray(); if (filtered.Length == 0) return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.NoneFound)); if (filtered.Length == 1) return Attempt.Succeed(new EventNameExtractorResult(filtered[0])); //there's more than one left so it's ambiguous! return Attempt.Fail(new EventNameExtractorResult(EventNameExtractorError.Ambiguous)); } /// /// Finds the event name on the sender that matches the args type /// /// /// /// /// A filter to exclude matched event names, this filter should return true to exclude the event name from being matched /// /// /// null if not found or an ambiguous match /// public static Attempt FindEvent(object sender, object args, Func exclude) { return FindEvent(sender.GetType(), args.GetType(), exclude); } /// /// Return true if the event is named with an ING name such as "Saving" or "RollingBack" /// /// /// internal static bool MatchIngNames(string eventName) { var splitter = new Regex(@"(? /// Return true if the event is not named with an ING name such as "Saving" or "RollingBack" /// /// /// internal static bool MatchNonIngNames(string eventName) { var splitter = new Regex(@"(? /// Used to cache all candidate events for a given type so we don't re-look them up /// private static readonly ConcurrentDictionary CandidateEvents = new ConcurrentDictionary(); /// /// Used to cache all matched event names by (sender type + arg type) so we don't re-look them up /// private static readonly ConcurrentDictionary, string[]> MatchedEventNames = new ConcurrentDictionary, string[]>(); } }