ServiceCollection Extension mit Dependency als Parameter?

  • C#
  • .NET 5–6

Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von slice.

    ServiceCollection Extension mit Dependency als Parameter?

    Hey,
    ich prügel mich gerade mal wieder mit Thema Dependency Injection bzw. der Extension-Method um den Service zu registrieren.
    Aktuelle Implementierung

    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient(this IServiceCollection services, IConfiguration proxyConfig, IConfiguration credentialConfig)
    2. {
    3. services.Configure<GraphAuthProxySettings>(proxyConfig);
    4. services.Configure<GraphAuthCredentialSettings>(credentialConfig);
    5. services.AddSingleton<IGraphAuthService, GraphAuthService>();
    6. services.AddSingleton<IIdentityLogger, GraphAuthIdentityLogger>();
    7. services.AddSingleton<IMsalHttpClientFactory, GraphAuthHttpClientFactory>();
    8. services.AddTransient<DelegatingHandler, GraphAuthTokenHandler>();
    9. services.AddSingleton(sp =>
    10. {
    11. var handlers = GraphClientFactory.CreateDefaultHandlers();
    12. handlers.Add(sp.GetRequiredService<GraphAuthTokenHandler>());
    13. handlers.Add(sp.GetRequiredService<HttpLoggingHandler>()); // um die Zeile geht es
    14. var httpClient = GraphClientFactory.Create(handlers);
    15. return new GraphServiceClient(httpClient);
    16. });
    17. return services;
    18. }

    Konkret geht es um folgende Zeile: handlers.Add(sp.GetRequiredService<HttpLoggingHandler>());

    Der Teil ist aktuell hardcoded, jedoch ist HttpLoggingHandler nicht Bestandteil der Library und soll veränderbar sein.
    Ich stehe gerade total auf dem Schlauch und krieg es nicht hin, das als Parameter zu realisieren.

    Was ich gefunden habe, wäre folgendes:
    Spoiler anzeigen

    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient<TService>(this IServiceCollection services, IConfiguration proxyConfig, IConfiguration credentialConfig, Func<IServiceProvider, TService> factory) where TService : DelegatingHandler
    2. {
    3. TService finalHandler = factory(services.BuildServiceProvider());
    4. // ...
    5. services.AddSingleton(sp =>
    6. {
    7. var handlers = GraphClientFactory.CreateDefaultHandlers();
    8. handlers.Add(sp.GetRequiredService<GraphAuthTokenHandler>());
    9. handlers.Add(finalHandler);
    10. var httpClient = GraphClientFactory.Create(handlers);
    11. return new GraphServiceClient(httpClient);
    12. });
    13. return services;
    14. }

    Aber services.BuildServiceProvider() aufzurufen ist keine gute Idee und bringt Probleme mit der Service LifeTime etc.

    Den DelegatingHandler direkt als Parameter zu übergeben ist auch problematisch, da er so von Hand erstellt werden muss und wir wieder beim Thema "Service LifeTime" wären.

    Irgendwer eine schlaue Idee?
    Doppelpost incoming :whistling:

    Ok, auf Stackoverflow wurde ich auf meine Blindheit hingewiesen ...
    Für einen DelegatingHandler ist das einfach
    Spoiler anzeigen

    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient<TService>(this IServiceCollection services,
    2. IConfiguration proxyConfig,
    3. IConfiguration credentialConfig) where TService : DelegatingHandler {
    4. // ...omitted for brevity
    5. services.AddSingleton(sp => {
    6. var handlers = GraphClientFactory.CreateDefaultHandlers();
    7. // ...
    8. handlers.Add(sp.GetRequiredService<TService>()); //<--resolve TService and add to handlers
    9. var httpClient = GraphClientFactory.Create(handlers);
    10. return new GraphServiceClient(httpClient);
    11. });
    12. return services;
    13. }


    Danach habe ich mich nochmal einige Stunden mit dem Thema beschäftigt.
    Wie könnte ich das lösen, wenn ich eine Liste von DelegatingHandler hinzufügen wollte und bin zu folgender Implementierung gekommen:
    Spoiler anzeigen

    C#-Quellcode

    1. internal class GraphHandlers
    2. {
    3. private List<Type> handlers;
    4. public IReadOnlyCollection<Type> Handlers { get { return this.handlers.AsReadOnly(); } }
    5. public GraphHandlers()
    6. {
    7. this.handlers = new List<Type>();
    8. }
    9. public GraphHandlers Add<THandler>() where THandler : DelegatingHandler
    10. {
    11. this.handlers.Add(typeof(THandler));
    12. return this;
    13. }
    14. }


    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient(this IServiceCollection services,
    2. IConfiguration proxyConfig,
    3. IConfiguration credentialConfig,
    4. GraphHandlers graphHandlers)
    5. {
    6. foreach (var handler in graphHandlers.Handlers)
    7. {
    8. services.AddTransient(handler);
    9. }
    10. // ..
    11. services.AddSingleton(sp =>
    12. {
    13. var handlers = GraphClientFactory.CreateDefaultHandlers();
    14. // ..
    15. foreach (var handler in graphHandlers.Handlers)
    16. {
    17. handlers.Add((DelegatingHandler)sp.GetRequiredService(handler));
    18. }
    19. var httpClient = GraphClientFactory.Create(handlers);
    20. return new GraphServiceClient(httpClient);
    21. });
    22. return services;
    23. }


    C#-Quellcode

    1. builder.Services.AddGraphClient(
    2. builder.Configuration.GetSection("Proxy"),
    3. builder.Configuration.GetSection("GraphCredentials"),
    4. new GraphHandlers().Add<WhatEverHandler>().Add<HttpLoggingHandler>()
    5. );

    Es funktioniert, nur ist die Frage ob das eine solide Implementierung ist?
    Irgendwelche Anmerkungen dazu?

    slice schrieb:

    Irgendwelche Anmerkungen dazu?
    Du gibst eine IServiceCollection hinein und returnst eine IServiceCollection - und zwar genau diejenige, die du hineingegeben hast.
    Wenn du nicht etwas besonderes damit bezweckst, sollte die Methode lieber void sein.

    Ansonsten alles höchst verschwurbelt: services bekommt die Handlers aus GraphHandlers.Handlers geadded.
    Und einen Singleton, der iwas mit GraphClientFactory.CreateDefaultHandlers() macht - da scheinbar noch iwelche anderen Handlers zufügt, die wiederum aus sp.GetRequiredService(handler)) gebildet wrden. Mit diese Handlers wird ein httpClient erzeugt, und von dem ein neuer GraphServidClient - das wird returnt - äh - von einer anonymen Methode, die den services zugefügt wird.

    Also wenn du da selbst durchblickst, ist das schoma sehr gut. Und wenns funktioniert umso besser. Ob das eine solide Implementierung ist - dazu müsste man mehr davon verstehen. Ausgesprochen unsolides ist mir nicht aufgefallen - ich würd mir nur etwas einfacheres wünschen, aber weiss natürlich nicht, ob das möglich ist.
    Das mit dem IServiceCollection als Rückgabewert ist einfach so ein Ding von Dependency-Injection damit man die aneinanderhängen kann.

    Das das alles etwas verschwurbelt ist, da geben ich dir recht, ist aber leider auch Microsoft und/oder meiner Unwissenheit wie man es besser lösen kann geschuldet?
    Um das etwas aufzubröseln:
    Ich brauch ein GraphServiceClient mit eigenen DelegatingHandler
    1. Generiere Default-Handler (Redirect, Compression etc) mittels GraphClientFactory.CreateDefaultHandlers()
    2. Füge eigene Handler hinzu handlers.Add((DelegatingHandler)sp.GetRequiredService(handler));
    3. Erstelle vorkonfigurierten HttpClient (mit deren Feature-Flags etc) für GraphServiceClient
    4. Registriere den Service - in meinem Fall als Singleton, da es keinen Sinn macht jedes mal eine neue Instanz zu erstellen


    Als zusätzliche Info, hier die Doku von Microsoft um einen benutzerdefinierten Client zu erstellen:
    Spoiler anzeigen

    C#-Quellcode

    1. // tokenCredential is one of the credential classes from Azure.Identity
    2. // scopes is an array of permission scope strings
    3. var authProvider = new AzureIdentityAuthenticationProvider(tokenCredential, scopes: scopes);
    4. var handlers = GraphClientFactory.CreateDefaultHandlers();
    5. // Remove a default handler
    6. // Microsoft.Kiota.Http.HttpClientLibrary.Middleware.CompressionHandler
    7. var compressionHandler =
    8. handlers.Where(h => h is CompressionHandler).FirstOrDefault();
    9. handlers.Remove(compressionHandler);
    10. // Add a new one
    11. // ChaosHandler simulates random server failures
    12. // Microsoft.Kiota.Http.HttpClientLibrary.Middleware.ChaosHandler
    13. handlers.Add(new ChaosHandler());
    14. var httpClient = GraphClientFactory.Create(handlers);
    15. var customGraphClient = new GraphServiceClient(httpClient, authProvider);


    Nachtrag bezüglich deinem "ich würd mir nur etwas einfacheres wünschen":
    Das ist genau der Grund, warum ich eine Extension-Method dafür schreiben möchte, da ich das so oft brauche und keine Lust habe es jedes Mal von neuem zu schreiben.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „slice“ ()

    Wenn du die Handler via DI registrieren möchtest, könnte es vielleicht so klappen? Ich kenne die Libs nicht, die du nutzt, aber wenn ich den Code richtig lese, sollten die Handler alle von DelegatingHandler erben, oder?

    C#-Quellcode

    1. services.AddSingleton(sp =>
    2. {
    3. var registeredHandlers = sp.GetRequiredServices<IEnumerable<DelegatingHandler>>();
    4. var defaultHandlers = GraphClientFactory.CreateDefaultHandlers();
    5. foreach (var registeredHandler in registeredHandlers)
    6. {
    7. defaultHandlers.Add(registeredHandler);
    8. }
    9. var httpClient = GraphClientFactory.Create(handlers);
    10. return new GraphServiceClient(httpClient);
    11. });
    12. // Handler dann so registrieren:
    13. services.AddSingleton<DelegatingHandler, HandlerA>();
    14. services.AddSingleton<DelegatingHandler, HandlerB>();
    15. services.AddSingleton<DelegatingHandler, HandlerC>();


    Hier musst du aufpassen, dass die Handler auch immer als Singleton registriert werden. Die Types beim Registrieren müssen auch passen (also DelegatingHandler muss in den Typeparams sein). Dafür könntest du auch easy ne Extension AddGraphHandler<THandler>(THandler handler) schreiben, die den Add Call macht.

    Alternativ könntest du überlegen, die Handler überhaupt nicht vom DI Container kommen zu lassen. Das wäre leichter, geht aber nur, wenn die Handler selbst keine DI Deps haben. Da du sowieso schon eine eigene Extension AddGraphClient hast, könntest du die Handler dort einfach dynamisch als Parameter akzeptieren. Beispielsweise so, per Factory Funktion:

    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient(
    2. this IServiceCollection services,
    3. /* ...andere Parameter... */,
    4. Func<IEnumerable<DelegatingHandler>> customHandlerFactory)
    5. {
    6. services.AddSingleton(sp =>
    7. {
    8. var customHandlers = customHandlerFactory();
    9. var defaultHandlers = GraphClientFactory.CreateDefaultHandlers();
    10. foreach (var customHandler in customHandlers)
    11. {
    12. defaultHandlers.Add(customHandler);
    13. }
    14. var httpClient = GraphClientFactory.Create(handlers);
    15. return new GraphServiceClient(httpClient);
    16. });
    17. return services;
    18. }
    19. // Aufruf dann so:
    20. services.AddGraphClient(
    21. // ...andere Parameter...
    22. customHandlerFactory: () => new DelegatingHandler[] { new HandlerA(), new HandlerB(), new HandlerC() });

    shad schrieb:

    ...sollten die Handler alle von DelegatingHandler erben, oder?

    Korrekt, aber alle registrieren DelegatingHandler hinzufügen ist kein gute Idee, da auch andere registriert sein könnten, die nichts mit Graph am Hut haben.

    shad schrieb:

    Hier musst du aufpassen, dass die Handler auch immer als Singleton registriert werden. Die Types beim Registrieren müssen auch passen

    DelegatingHandler sind da etwas merkwürdig mit ihrer Service Lifetime, hier ein Link der das relativ gut erklärt: DI scopes in IHttpClientFactory message handlers don't work like you think they do
    Wobei das in meinem Fall nicht sonderlich relevant sein sollte, da die Handler stateless sind.

    shad schrieb:

    Alternativ könntest du überlegen, die Handler überhaupt nicht vom DI Container kommen zu lassen. Das wäre leichter, geht aber nur, wenn die Handler selbst keine DI Deps haben.

    Das ist leider ein Problem, einige Handler haben Abhängigkeiten :(
    Hmm, das ist dann echt ein spezielleres Problem. Auf die Schnelle gedacht, würde sowas reichen? Ist eine leicht abgewandelte Version von meinem vorherigen zweiten Fragment, mit dem Unterschied, dass die Factory jetzt Services vom ServiceProvider holen kann. Damit könnte der Aufrufer der AddGraphClient Extension spezifische Services auswählen, die gleichzeitig vom DI Container kommen:

    C#-Quellcode

    1. internal static IServiceCollection AddGraphClient(
    2. this IServiceCollection services,
    3. /* ...andere Parameter... */,
    4. Func<IServiceProvider, IEnumerable<DelegatingHandler>> customHandlerFactory)
    5. {
    6. services.AddSingleton(sp =>
    7. {
    8. var customHandlers = customHandlerFactory(sp);
    9. var defaultHandlers = GraphClientFactory.CreateDefaultHandlers();
    10. foreach (var customHandler in customHandlers)
    11. {
    12. defaultHandlers.Add(customHandler);
    13. }
    14. var httpClient = GraphClientFactory.Create(handlers);
    15. return new GraphServiceClient(httpClient);
    16. });
    17. return services;
    18. }
    19. // Aufruf dann so:
    20. services.AddGraphClient(
    21. // ...andere Parameter...
    22. customHandlerFactory: (sp) =>
    23. new DelegatingHandler[]
    24. {
    25. sp.GetRequiredService<HandlerA>(),
    26. sp.GetRequiredService<HandlerB>(),
    27. sp.GetRequiredService<HandlerC>(),
    28. new HandlerD(),
    29. });
    Arg, wo warst du bitte vor ca. vier Wochen?
    Die Variante hätte natürlich den Charme, das ich mir die zusätzliche Klasse spare.

    Danke schön!