dolittle/DotNET.SDK

View on GitHub
Source/Events.Handling/Builder/Convention/ConventionEventHandlerBuilder.cs

Summary

Maintainability
B
7 hrs
Test Coverage
F
0%
// Copyright (c) Dolittle. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Dolittle.SDK.Common.ClientSetup;
using Dolittle.SDK.Events.Handling.Builder.Methods;

namespace Dolittle.SDK.Events.Handling.Builder.Convention;

/// <summary>
/// Methods for building <see cref="IEventHandler"/> instances by convention from an instantiated event handler class.
/// </summary>
public abstract class ConventionEventHandlerBuilder : ICanTryBuildEventHandler, IEquatable<ICanTryBuildEventHandler>
{
    const string MethodName = "Handle";
    
    readonly EventHandlerModelId _identifier;
    readonly object _eventHandlerInstance;

    /// <summary>
    /// Initializes a new instance of the <see cref="ConventionEventHandlerBuilder"/> class.
    /// </summary>
    /// <param name="identifier">The <see cref="EventHandlerAttribute"/> decorator.</param>
    /// <param name="eventHandlerType">The event handler <see cref="Type" />.</param>
    /// <param name="eventHandlerInstance">The optional instance of the event handler class.</param>
    protected ConventionEventHandlerBuilder(EventHandlerModelId identifier, System.Type eventHandlerType, object eventHandlerInstance = default)
    {
        EventHandlerType = eventHandlerType;
        _identifier = identifier;
        _eventHandlerInstance = eventHandlerInstance;
    }

    /// <summary>
    /// Gets the <see cref="Type" /> of the event handler.
    /// </summary>
    public System.Type EventHandlerType { get; }

    /// <inheritdoc />
    public abstract bool TryBuild(
        EventHandlerModelId identifier,
        IEventTypes eventTypes,
        IClientBuildResults buildResults,
        out IEventHandler eventHandler);

    /// <inheritdoc />
    public bool Equals(ICanTryBuildEventHandler other)
    {
        if (other == null)
        {
            return false;
        }
        
        if (ReferenceEquals(this, other))
        {
            return true;
        }

        return other is ConventionEventHandlerBuilder otherBuilder
            && EventHandlerType == otherBuilder.EventHandlerType
            && _eventHandlerInstance.Equals(otherBuilder._eventHandlerInstance);
    }

    /// <inheritdoc />
    public override int GetHashCode()
        => HashCode.Combine(_identifier, _eventHandlerInstance, EventHandlerType);
    
    /// <summary>
    /// Builds event handler.
    /// </summary>
    /// <param name="eventTypes">The <see cref="IEventTypes" />.</param>
    /// <param name="createUntypedHandlerMethod">The <see cref="CreateUntypedHandleMethod" /> callback.</param>
    /// <param name="createTypedHandlerMethod">The <see cref="CreateTypedHandleMethod" /> callback.</param>
    /// <param name="buildResults">The <see cref="IClientBuildResults"/>.</param>
    /// <param name="eventHandler">The built <see cref="IEventHandler"/>.</param>
    protected bool TryBuild(
        IEventTypes eventTypes,
        CreateUntypedHandleMethod createUntypedHandlerMethod,
        CreateTypedHandleMethod createTypedHandlerMethod,
        IClientBuildResults buildResults,
        out IEventHandler eventHandler)
    {
        eventHandler = default;
        buildResults.AddInformation(_identifier, $"Building from type {EventHandlerType}");

        var eventTypesToMethods = new Dictionary<EventType, IEventHandlerMethod>();

        if (!TryBuildHandlerMethods(
                _identifier.Id,
                eventTypes,
                createUntypedHandlerMethod,
                createTypedHandlerMethod,
                eventTypesToMethods,
                buildResults))
        {
            return false;
        }

        eventHandler = new EventHandler(_identifier, eventTypesToMethods);

        return true;
    }

    bool TryBuildHandlerMethods(
        EventHandlerId eventHandlerId,
        IEventTypes eventTypes,
        CreateUntypedHandleMethod createUntypedHandlerMethod,
        CreateTypedHandleMethod createTypedHandlerMethod,
        IDictionary<EventType, IEventHandlerMethod> eventTypesToMethods,
        IClientBuildResults buildResults)
    {
        var allDeclaredMethods = EventHandlerType.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic);
        
        var hasWrongMethods = !TryAddDecoratedHandlerMethods(
                allDeclaredMethods,
                eventHandlerId,
                createUntypedHandlerMethod,
                eventTypesToMethods,
                buildResults)
            | !TryAddConventionHandlerMethods(
                allDeclaredMethods,
                eventHandlerId,
                eventTypes,
                createTypedHandlerMethod,
                eventTypesToMethods,
                buildResults);

        if (hasWrongMethods)
        {
            return false;
        }

        if (eventTypesToMethods.Count != 0)
        {
            return true;
        }
        buildResults.AddFailure(_identifier, $"There are no event handler methods to register in event handler {EventHandlerType}. An event handler method either needs to be decorated with [{nameof(HandlesAttribute)}] or have the name {MethodName}");
        return false;
    }

    bool TryAddDecoratedHandlerMethods(
        IEnumerable<MethodInfo> methods,
        EventHandlerId eventHandlerId,
        CreateUntypedHandleMethod createUntypedHandlerMethod,
        IDictionary<EventType, IEventHandlerMethod> eventTypesToMethods,
        IClientBuildResults buildResults)
    {
        var allMethodsAdded = true;
        foreach (var method in methods.Where(IsDecoratedHandlerMethod))
        {
            var shouldAddHandler = true;
            var eventType = (method.GetCustomAttributes(typeof(HandlesAttribute), true)[0] as HandlesAttribute)?.EventType;
            if (!TryGetFirstMethodParameterType(method, out var eventParameterType))
            {
                buildResults.AddFailure(_identifier, $"{method} has no parameters, but is decorated with [{nameof(HandlesAttribute)}]. An event handler method should take in as parameters an event and an {nameof(EventContext)}");
                shouldAddHandler = false;
            }

            if (!ParametersAreOkay(method, buildResults))
            {
                shouldAddHandler = false;
            }

            if (eventParameterType != typeof(object))
            {
                buildResults.AddFailure(_identifier, $"{method} should only handle an event of type object");
                shouldAddHandler = false;
            }

            if (!method.IsPublic)
            {
                buildResults.AddFailure(_identifier, $"{method} has the signature of an event handler method, but is not public. Event handler methods needs to be public");
                shouldAddHandler = false;
            }

            switch (shouldAddHandler)
            {
                case true when !eventTypesToMethods.TryAdd(eventType, createUntypedHandlerMethod(method)):
                    allMethodsAdded = false;
                    buildResults.AddFailure(_identifier, $"Multiple handlers for {eventType}");
                    break;
                case false:
                    allMethodsAdded = false;
                    break;
            }
        }
        return allMethodsAdded;
    }

    bool TryAddConventionHandlerMethods(
        IEnumerable<MethodInfo> methods,
        EventHandlerId eventHandlerId,
        IEventTypes eventTypes,
        CreateTypedHandleMethod createTypedHandlerMethod,
        IDictionary<EventType, IEventHandlerMethod> eventTypesToMethods,
        IClientBuildResults buildResults)
    {
        var allMethodsAdded = true;
        foreach (var method in methods.Where(_ => !IsDecoratedHandlerMethod(_) && _.Name == MethodName))
        {
            var shouldAddHandler = true;
            if (!TryGetFirstMethodParameterType(method, out var eventParameterType))
            {
                buildResults.AddFailure(_identifier, $"{method} has no parameters. An event handler method should take in as parameters an event and an {nameof(EventContext)}");
                shouldAddHandler = false;
            }

            if (eventParameterType == typeof(object))
            {
                buildResults.AddFailure(_identifier, $"{method} cannot handle an untyped event when not decorated with [{nameof(HandlesAttribute)}]");
                shouldAddHandler = false;
            }

            if (!eventTypes.HasFor(eventParameterType))
            {
                buildResults.AddFailure(_identifier, $"{method} handles event of type {eventParameterType}, but it is not associated to any event type");
                shouldAddHandler = false;
            }

            if (!ParametersAreOkay(method, buildResults))
            {
                shouldAddHandler = false;
            }

            if (!method.IsPublic)
            {
                buildResults.AddFailure(_identifier, $"{method} has the signature of an event handler method, but is not public. Event handler methods needs to be public");
                shouldAddHandler = false;
            }

            switch (shouldAddHandler)
            {
                case true when !eventTypesToMethods.TryAdd(eventTypes.GetFor(eventParameterType), createTypedHandlerMethod(eventParameterType, method)):
                    allMethodsAdded = false;
                    buildResults.AddFailure(_identifier, $"Multiple handlers for {eventParameterType}");
                    break;
                case false:
                    allMethodsAdded = false;
                    break;
            }
        }
        return allMethodsAdded;
    }

    bool ParametersAreOkay(MethodInfo method, IClientBuildResults buildResults)
    {
        var hasErrors = false;
        if (!SecondMethodParameterIsEventContext(method))
        {
            buildResults.AddFailure(_identifier, $"{method} needs to have two parameters where the second parameter is {nameof(EventContext)}");
            hasErrors = true;
        }

        if (!MethodHasNoExtraParameters(method))
        {
            buildResults.AddFailure(_identifier, $"{method} needs to only have two parameters where the first is the event to handle and the second is {nameof(EventContext)}");
            hasErrors = true;
        }

        if (MethodReturnsAsyncVoid(method) || (!MethodReturnsVoid(method) && !MethodReturnsTask(method)))
        {
            buildResults.AddFailure(_identifier, $"{method} needs to return either {typeof(void)} or {typeof(Task)}");
            hasErrors = true;
        }

        return !hasErrors;
    }
    
    static bool TryGetFirstMethodParameterType(MethodInfo method, out System.Type type)
    {
        type = default;
        if (method.GetParameters().Length == 0)
        {
            return false;
        }

        type = method.GetParameters()[0].ParameterType;
        return true;
    }

    static bool IsDecoratedHandlerMethod(MethodInfo method)
        => method.GetCustomAttributes(typeof(HandlesAttribute), true).FirstOrDefault() != default;

    static bool SecondMethodParameterIsEventContext(MethodInfo method)
        => method.GetParameters().Length > 1 && method.GetParameters()[1].ParameterType == typeof(EventContext);

    static bool MethodHasNoExtraParameters(MethodInfo method)
        => method.GetParameters().Length == 2;

    static bool MethodReturnsTask(MethodInfo method)
        => method.ReturnType == typeof(Task);

    static bool MethodReturnsVoid(MethodInfo method)
        => method.ReturnType == typeof(void);

    static bool MethodReturnsAsyncVoid(MethodInfo method)
    {
        var asyncAttribute = typeof(AsyncStateMachineAttribute);
        var isAsyncMethod = (AsyncStateMachineAttribute)method.GetCustomAttribute(asyncAttribute) != null;
        return isAsyncMethod && MethodReturnsVoid(method);
    }
}