End-to-End-Funktionalität über Wrapper

Während der Entwicklung stoßen wir häufig auf eine Situation, in der beim Ausführen einer Geschäftslogik Protokolle, Audits und Warnungen gesendet werden müssen. Implementieren Sie im Allgemeinen einige End-to-End-Funktionen.



Wenn der Produktionsumfang klein ist, können Sie nicht zu eifrig sein und all dies in den Methoden richtig machen. Allmählich wächst der Servicekonstruktor mit eingehenden Services für die Implementierung von BL und End-to-End-Funktionen. Und das ist ILogger, IAuditService, INotifySerice.

Ich weiß nichts über dich, aber ich mag nicht viele Injektionen und große Methoden, die viele Aktionen gleichzeitig ausführen.



Sie können jede AOP-Implementierung im Code beenden. Im .NET-Stack werden solche Implementierungen an den richtigen Stellen in Ihre Anwendung eingefügt, sehen aus wie Magie der Stufe 80 und haben häufig Probleme beim Schreiben und Debuggen.



Ich habe versucht, einen Mittelweg zu finden. Wenn diese Probleme Sie nicht verschont haben, begrüßen Sie unter Katze.



Spoiler. Tatsächlich konnte ich etwas mehr Probleme lösen als oben beschrieben. Zum Beispiel kann ich die Entwicklung von BL zu einem Entwickler geben, und hängenden End-to-End - Funktionalität und auch die Validierung der eingehenden Daten - zum anderen zugleich .



Und Dekorateure und ein DI-Add-In haben mir dabei geholfen. Jemand wird weiter sagen, dass dies ein Proxy ist, ich werde dies gerne in den Kommentaren diskutieren.



Was möchte ich als Entwickler?



  • Lassen Sie sich bei der Implementierung von BL nicht von der linken Funktion ablenken.
  • In Unit-Tests nur BL testen können. Und ich mache nicht gerne 100500 Mocks, um alle Zusatzfunktionen zu deaktivieren. 2-3 - okay, aber ich will nicht.
  • Verstehe, was passiert, ohne 7 Felder in deiner Stirn zu haben. :) :)
  • In der Lage sein, die Lebensdauer des Dienstes und jedes seiner Wrapper SEPARAT zu verwalten!


Was möchte ich als Designer und Teamleiter?



  • In der Lage zu sein, Aufgaben optimal und mit der geringsten Kohärenz zu zerlegen, so dass gleichzeitig so viele Entwickler wie möglich an verschiedenen Aufgaben beteiligt werden können und gleichzeitig so wenig Zeit wie möglich für die Forschung aufgewendet wird (wenn der Entwickler eine BL entwickeln muss und gleichzeitig darüber nachdenkt, was und wie gesichert werden soll) Er wird mehr Zeit für die Forschung aufwenden. Und so ist es mit jedem Stück BL viel einfacher, Audit-Aufzeichnungen zu erfassen und sie während des gesamten Projekts zu stopfen.
  • Behalten Sie die Reihenfolge bei, in der der Code getrennt von seiner Entwicklung ausgeführt wird.


Diese Schnittstelle wird mir dabei helfen.



    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="T">  . </typeparam>
    public interface IDecorator<T>
    {
        /// <summary>
        ///        .
        /// </summary>
        Func<T> NextDelegate { get; set; }
    }


Sie können so etwas verwenden
interface IService
{
    Response Method(Request request);
}

class Service : IService
{
    public Response Method(Request request)
    {
        // BL
    }
}

class Wrapper : IDecorator<IService>, IService
{
    public Func<IService> NextDelegate { get; set; }

    public Response Method(Request request)
    {
        // code before
        var result = NextDelegate().Method(request);
        // code after
        return result;
    }
}




So wird unser Handeln tiefer gehen.



wrapper1
    wrapper2
        service
    end wrapper2
end wrapper1


Aber warte. Dies ist bereits in OOP und wird als Vererbung bezeichnet. : D.



class Service {}
class Wrapper1: Service {}
class Wrapper2: Wrapper1 {}


Als ich mir vorstellte, dass zusätzliche End-to-End-Funktionen erscheinen würden, die in der gesamten Anwendung in der Mitte implementiert werden müssten, oder um die vorhandenen auszutauschen, standen die Haare auf meinem Rücken zu Berge.



Aber meine Faulheit ist kein guter Grund. Der gute Grund ist, dass es beim Testen der Funktionalität in den Klassen Wrapper1 und Wrapper2 große Probleme geben wird, während NextDelegate in meinem Beispiel einfach verspottet werden kann. Darüber hinaus verfügen der Service und jeder Wrapper über eigene Tools, die in den Konstruktor eingefügt werden, während der letzte Wrapper bei der Vererbung über unnötige Tools verfügen muss, um sie an die Eltern weiterzugeben.



Wenn der Ansatz akzeptiert wird, muss noch herausgefunden werden, wo, wie und wann NextDelegate zugewiesen werden soll.



Ich entschied, dass die logischste Lösung darin besteht, Dienste dort zu registrieren. (Startup.sc, Standard).



So sieht es in der Basisversion aus.
            services.AddScoped<Service>();
            services.AddTransient<Wrapper1>();
            services.AddSingleton<Wrapper2>();
            services.AddSingleton<IService>(sp =>
            {
                var wrapper2 = sp.GetService<Wrapper2>();
                wrapper2.NextDelegate = () =>
                {
                    var wrapper1 = sp.GetService<Wrapper1>();
                    wrapper1.NextDelegate = () =>
                    {
                        return sp.GetService<Service>();
                    };

                    return wrapper1;
                };

                return wrapper2;
            });




Im Allgemeinen wurden alle Anforderungen erfüllt, aber ein anderes Problem trat auf - das Verschachteln.



Dieses Problem kann durch rohe Gewalt oder Rekursion gelöst werden. Aber unter der Haube. Äußerlich sollte alles einfach und verständlich aussehen.



Das habe ich erreicht
            services.AddDecoratedScoped<IService, Service>(builder =>
            {
                builder.AddSingletonDecorator<Wrapper1>();
                builder.AddTransientDecorator<Wrapper2>();
                builder.AddScopedDecorator<Wrapper3>();
            });




Und diese Erweiterungsmethoden haben mir dabei geholfen.



Und diese Erweiterungsmethoden und der Szenerie-Builder haben mir dabei geholfen.
    /// <summary>
    ///        .
    /// </summary>
    public static class DecorationExtensions
    {
        /// <summary>
        ///        .
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="lifeTime"></param>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecorated<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection, ServiceLifetime lifeTime,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            var builder = new DecorationBuilder<TDefinition>();
            decorationBuilder(builder);

            var types = builder.ServiceDescriptors.Select(k => k.ImplementationType).ToArray();

            var serviceDescriptor = new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), lifeTime);

            serviceCollection.Add(serviceDescriptor);

            foreach (var descriptor in builder.ServiceDescriptors)
            {
                serviceCollection.Add(descriptor);
            }

            var resultDescriptor = new ServiceDescriptor(typeof(TDefinition),
                ConstructServiceFactory<TDefinition>(typeof(TImplementation), types), ServiceLifetime.Transient);
            serviceCollection.Add(resultDescriptor);

            return serviceCollection;
        }

        /// <summary>
        ///            Scoped.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedScoped<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Scoped,
                decorationBuilder);
        }

        /// <summary>
        ///            Singleton.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedSingleton<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Singleton,
                decorationBuilder);
        }

        /// <summary>
        ///            Transient.
        /// </summary>
        /// <typeparam name="TDefinition">  . </typeparam>
        /// <typeparam name="TImplementation">  . </typeparam>
        /// <param name="serviceCollection">  . </param>
        /// <param name="decorationBuilder">  . </param>
        /// <returns>     . </returns>
        public static IServiceCollection AddDecoratedTransient<TDefinition, TImplementation>(
            this IServiceCollection serviceCollection,
            Action<DecorationBuilder<TDefinition>> decorationBuilder)
            where TImplementation : TDefinition
        {
            return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Transient,
                decorationBuilder);
        }

        /// <summary>
        ///     
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <param name="implType"></param>
        /// <param name="next"></param>
        /// <returns></returns>
        private static Func<IServiceProvider, TService> ConstructDecorationActivation<TService>(Type implType,
            Func<IServiceProvider, TService> next)
        {
            return x =>
            {
                var service = (TService) x.GetService(implType);

                if (service is IDecorator<TService> decorator)
                    decorator.NextDelegate = () => next(x);
                else
                    throw new InvalidOperationException(" ");

                return service;
            };
        }

        /// <summary>
        ///         .
        /// </summary>
        /// <typeparam name="TDefinition">   . </typeparam>
        /// <param name="serviceType">   . </param>
        /// <param name="decoratorTypes">     . </param>
        /// <returns>     DI. </returns>
        private static Func<IServiceProvider, object> ConstructServiceFactory<TDefinition>(Type serviceType,
            Type[] decoratorTypes)
        {
            return sp =>
            {
                Func<IServiceProvider, TDefinition> currentFunc = x =>
                    (TDefinition) x.GetService(serviceType);
                foreach (var decorator in decoratorTypes)
                {
                    currentFunc = ConstructDecorationActivation(decorator, currentFunc);
                }

                return currentFunc(sp);
            };
        }
    }




    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="TService">  . </typeparam>
    public class DecorationBuilder<TService>
    {
        private readonly List<ServiceDescriptor> _serviceDescriptors = new List<ServiceDescriptor>();

        /// <summary>
        ///       .
        /// </summary>
        public IReadOnlyCollection<ServiceDescriptor> ServiceDescriptors => _serviceDescriptors;

        /// <summary>
        ///      .
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        /// <param name="lifeTime">   . </param>
        public void AddDecorator<TDecorator>(ServiceLifetime lifeTime) where TDecorator : TService, IDecorator<TService>
        {
            var container = new ServiceDescriptor(typeof(TDecorator), typeof(TDecorator), lifeTime);
            _serviceDescriptors.Add(container);
        }

        /// <summary>
        ///          Scoped.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddScopedDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Scoped);
        }

        /// <summary>
        ///          Singleton.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddSingletonDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Singleton);
        }

        /// <summary>
        ///          Transient.
        /// </summary>
        /// <typeparam name="TDecorator">  . </typeparam>
        public void AddTransientDecorator<TDecorator>() where TDecorator : TService, IDecorator<TService>
        {
            AddDecorator<TDecorator>(ServiceLifetime.Transient);
        }
    }




Nun etwas Zucker für Funktionalisten



Nun etwas Zucker für Funktionalisten
    /// <summary>
    ///       .
    /// </summary>
    /// <typeparam name="T">   . </typeparam>
    public class DecoratorBase<T> : IDecorator<T>
    {
        /// <summary>
        ///           .
        /// </summary>
        public Func<T> NextDelegate { get; set; }

        /// <summary>
        ///           .
        /// </summary>
        /// <typeparam name="TResult">   . </typeparam>
        /// <param name="lambda">  . </param>
        /// <returns></returns>
        protected Task<TResult> ExecuteAsync<TResult>(Func<T, Task<TResult>> lambda)
        {
            return lambda(NextDelegate());
        }

        /// <summary>
        ///           .
        /// </summary>
        /// <param name="lambda">  . </param>
        /// <returns></returns>
        protected Task ExecuteAsync(Func<T, Task> lambda)
        {
            return lambda(NextDelegate());
        }
    }


, , ,



    public Task<Response> MethodAsync(Request request)
    {
        return ExecuteAsync(async next =>
        {
            // code before
            var result = await next.MethodAsync(request);
            // code after
            return result;
        });
    }


, :



    public Task<Response> MethodAsync(Request request)
    {
        return ExecuteAsync(next => next.MethodAsync(request));
    }




Es ist noch ein wenig Magie übrig. Der Zweck der NextDelegate-Eigenschaft. Es ist nicht sofort klar, was es ist und wie man es benutzt, aber ein erfahrener Programmierer wird es finden, aber ein Unerfahrener muss es einmal erklären. Es ist wie bei DbSets in DbContext.



Ich habe es nicht auf den Git Hub gelegt. Es gibt nicht viel Code, er ist bereits verallgemeinert, sodass Sie ihn direkt von hier aus abrufen können.



Abschließend möchte ich nichts sagen.



All Articles