Laden...

Hilfe bei Dependency Injection und Services

Erstellt von Sandmann vor einem Jahr Letzter Beitrag vor einem Jahr 708 Views
S
Sandmann Themenstarter:in
8 Beiträge seit 2023
vor einem Jahr
Hilfe bei Dependency Injection und Services

Ahoi zusammen!

Ich versuche mich derzeit ein wenig in DotNet Core und C# einzuarbeiten. Ich hab meine Wurzeln in der Java-Ecke, schlag mich aber schon ein paar Jahre mit Softwareentwicklung herum. Es kann hier dennoch sein, dass mir gar kräftig Grundlagen fehlen. Ich bin bereit, die zu lernen. Wenn zur Beantwortung ein gepflegtes RTFM mit geeignetem Link besser geeignet ist, bitte also immer her damit.

Nun mal zu meinem Vorhaben:

Ich möchte einen generischen Service-Runner implementieren, der einen Hosted Service abstrahiert. Ob die Kiste was tut, soll ihm ein zweiter Service mitteilen und was dann wirklich getan wird, soll eine Worker-Klasse regeln.

Code dazu ist derzeit:


using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MyService.Core;

public interface IWorker{
    public Task DoSomething();
}

public interface IWorkerTicketProvider<TWorker>
    where TWorker:IWorker
{
    public ValueTask<bool> WaitForNextTicket(CancellationToken stoppingToken);
}

  
public class MyServiceRunner<TWorker,TWorkerTicketProvider> : BackgroundService, IDisposable 
    where TWorker:IWorker 
    where TWorkerTicketProvider:IWorkerTicketProvider<TWorker>
{
    private readonly ILogger<MyServiceRunner<TWorker,TWorkerTicketProvider>> _logger;
    private readonly IWorkerTicketProvider<TWorker> _ticketProvider;

    private readonly IServiceScopeFactory _serviceScopeFactory;
    public bool IsEnabled { get; set; }

    public MyServiceRunner(
            ILogger<MyServiceRunner<TWorker,TWorkerTicketProvider>> myLogger,
            IServiceScopeFactory myScopeFactory,
            IWorkerTicketProvider<TWorker> myTicketProvider)
    {

        this._logger=myLogger;
        this._serviceScopeFactory=myScopeFactory;
        this._ticketProvider=myTicketProvider;
        this.IsEnabled=true;

    }
    public override Task StartAsync(CancellationToken cancellationToken)  
    {  
        this._logger.LogInformation("Starting runner for "+typeof(TWorker).FullName);
        return base.StartAsync(cancellationToken);    
    }  
    
    public override Task StopAsync(CancellationToken cancellationToken)  
    {  
        // shutdown environment 
        this._logger.LogInformation("Stopping runner for "+typeof(TWorker).FullName);
        return base.StopAsync(cancellationToken);  
    }  
   protected override async Task ExecuteAsync(CancellationToken stoppingToken)
   {
       while 
       (
           !stoppingToken.IsCancellationRequested &&
                       await this._ticketProvider.WaitForNextTicket(stoppingToken)
       )
       {
           try
           {
               if (IsEnabled)
               {
                   await using AsyncServiceScope asyncScope = this._serviceScopeFactory.CreateAsyncScope();
                   TWorker worker = asyncScope.ServiceProvider.GetRequiredService<TWorker>();
                   await worker.DoSomething();
                   _logger.LogInformation(
                       "Executed worker loop for "+typeof(TWorker).FullName);
               }
           }
           catch (Exception ex)
           {
               _logger.LogError(
                   "Failed to execute worker for "+typeof(TWorker).FullName+$" with exception {ex.Message}.");
           }
       }
   }
}


das ganze wird flankiert von einem Service-Worker:


using MyService.Core;

namespace MyServiceProgram;


public class MyWorker : IWorker{

    private readonly ILogger<MyWorker> _logger;

    public MyWorker(ILogger<MyWorker> myLogger){
        this._logger=myLogger;
    }

    public async Task DoSomething(){
        await Task.Delay(100);
        this._logger.LogInformation("did something");
    }
}

und einem Ticket-Provider:


using MyService.Core;

namespace MyServiceProgram;

public class MyTicketProvider : IWorkerTicketProvider<MyWorker>
{
    private readonly TimeSpan _period = TimeSpan.FromSeconds(5);

    public ValueTask<bool> WaitForNextTicket(CancellationToken stoppingToken)
    {
        using PeriodicTimer timer = new PeriodicTimer(_period);
        return timer.WaitForNextTickAsync(stoppingToken);
    }
    private readonly ILogger<MyTicketProvider> _logger;

    public MyTicketProvider(ILogger<MyTicketProvider> myLogger){
        this._logger=myLogger;
    }

}

Mein Versuch, das Spielkind zu triggern, sieht wie folgt aus:


using MyService.Core;

namespace MyServiceProgram;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddTransient<MyWorker>();
        builder.Services.AddScoped<MyTicketProvider>();
        builder.Services.AddSingleton<MyServiceRunner<MyWorker,MyTicketProvider>>();
        builder.Services.AddHostedService(
            provider => provider.GetRequiredService<MyServiceRunner<MyWorker,MyTicketProvider>>()
        );
        var app = builder.Build();

        app.Run();
    }
}


Im Ergebnis krieg ich nun die Meldung zurück, System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor.

bzw. in Lang:

Fehlermeldung:
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider] Lifetime: Singleton ImplementationType: MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]': Unable to resolve service for type 'MyService.Core.IWorkerTicketProvider1[MyServiceProgram.MyWorker]&#39; while attempting to activate &#39;MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]'.)
---> System.InvalidOperationException: Error while validating the service descriptor 'ServiceType: MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider] Lifetime: Singleton ImplementationType: MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]': Unable to resolve service for type 'MyService.Core.IWorkerTicketProvider1[MyServiceProgram.MyWorker]&#39; while attempting to activate &#39;MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]'.
---> System.InvalidOperationException: Unable to resolve service for type 'MyService.Core.IWorkerTicketProvider1[MyServiceProgram.MyWorker]&#39; while attempting to activate &#39;MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]'.

Und nun verließen sie ihn. Ich hätte jetzt gesagt, der Service ist identifizierbar, injizierbar und instanzierbar. Googlen nach Fehlermeldung bietet mir zu viele irrelevante Ergebnisse.

Könnt Ihr mir mal mit dem Holzpfosten ins Genick hauen, wo ich da jetzt am besten ansetze und am besten weiter recherchiere?

Danke + Gruß
Jens

F
10.010 Beiträge seit 2004
vor einem Jahr

Schau doch mal was die exception sagt.

Du hast einen Singleton, dein MyServiceRunner und willst ihm einen Scoped Service MyTicketProvider inserten, das geht nicht.

S
Sandmann Themenstarter:in
8 Beiträge seit 2023
vor einem Jahr

Vielen Dank für Deinen Hinweis. Ich hatte auch schon das hier probiert:


        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddTransient<MyWorker>();
        builder.Services.AddSingleton<MyTicketProvider>();
        builder.Services.AddSingleton<MyServiceRunner<MyWorker,MyTicketProvider>>();

Das ändert nur die Fehlermeldung keinen Millimeter.

Dreh und Angelpunkt des Problems scheint mir dieser Teil des Stacktraces zu sein:

Fehlermeldung:
Unable to resolve service for type 'MyService.Core.IWorkerTicketProvider1[MyServiceProgram.MyWorker]&#39; while attempting to activate &#39;MyService.Core.MyServiceRunner2[MyServiceProgram.MyWorker,MyServiceProgram.MyTicketProvider]'.

Das System scheint nicht in der Lage zu sein, aus der Notwendigkeit eines IWorkerTicketProvider<MyWorker> auf den existierenden MyTicketProvider zu schließen.

Ich hab daraufhin eben mal das hier probiert:


        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddTransient<MyWorker>();
        builder.Services.AddSingleton<IWorkerTicketProvider<MyWorker>,MyTicketProvider>();
        builder.Services.AddSingleton<MyServiceRunner<MyWorker,MyTicketProvider>>();

Das funktionierte dann auch. Bringt mich aber halt nur begrenzt weiter, weil ich noch nicht verstehe, nach welcher Logik die Auflösung der Services hier funktioniert. In meinen dicken Schädel will hier nicht rein, warum der Schluss vom benötigten Interface auf seine einzige, als Service existierende Implementierung nicht funktioniert. Bei MyWorker und MyServiceRunner<MyWorker,MyTicketProvider> musste ich das Interface ja auch nicht explizit angeben.

Die hier hab ich mittlerweile durch:

Das ist irgendwie alles ein bisschen zu Beispielgetrieben für meinen Kopf. Ist irgendwer von Euch mal über irgendwas im Netz gestolpert, was das Logik-Konzept hinter der Service-Auflösung ein wenig mehr erklärt? Oder kann mir wer sagen, was in dem Kontext der logische Unterschied zwischen den beiden hier ist:


        builder.Services.AddSingleton<IWorkerTicketProvider<MyWorker>,MyTicketProvider>();
        builder.Services.AddSingleton<MyServiceRunner<MyWorker,MyTicketProvider>>();

Vielen Dank für jeden sachdienlichen Hinweis.

Gruß Jens

126 Beiträge seit 2023
vor einem Jahr

Eigentlich ist es ganz einfach:

Jeder Typ muss explizit im DI-Container registriert werden, damit dieser auch aufgelöst werden kann.

Wenn du also den Typ IWorkerTicketProvider<MyWorker> irgendwo injecten möchtest, dann musst du IWorkerTicketProvider<MyWorker> auch explizit registrieren, so wie du das jetzt eben gemacht hast.

Hat die Blume einen Knick, war der Schmetterling zu dick.

16.832 Beiträge seit 2008
vor einem Jahr

Explizit zu registrieren ist meistens besser, aber keine Pflicht.
Generics werden unterstützt und sind in vielen Bibliotheken "Alltag". Beispiel aus MediatR dazu (weil ichs gestern auch gebraucht hab): MediatR.Registration.ServiceRegistrar


services.AddTransient(typeof(MyInterface<>),typeof(MyImplementation<>));

Und für komplexere Dinge gibts zB auch Erweiterungen wie https://github.com/khellang/Scrutor, die vor allem Decorate mitbringt.

Oder kann mir wer sagen, was in dem Kontext der logische Unterschied zwischen den beiden hier ist:

Auf YouTube gibts eigentlich durchaus einige Videos zum Thema Dependency Injection von durchaus bekannten und etablierten Personen.
Ich verweise einfach mal auf Dependency Injection in .NET Core (.NET 6) ohne den Inhalt zu kennen. Tim Corey ist ziemlich bekannt und hat viele Videos zu Basics, auch mehrere zu DI.

In Deinem Fall ist es vereinfacht ausgedrückt einfach so:
Ein DI Container sammelt keine Namen, sondern einfach Typen in einer Mapping Tabelle.

  • Typ A
  • Typ B
    ... und manchmal eben auch interfaces dazu wie
  • Interface A hat Implementierung A
  • Interface A hat Implementierung B, C und D

Und IWorkerTicketProvider<MyWorker> ist technisch gesehen einfach ein anderer Type als MyServiceRunner<MyWorker,MyTicketProvider>. Völlig egal, wie die Hierarchie ist.


builder.Services.AddSingleton<IWorkerTicketProvider<MyWorker>,MyTicketProvider>();

Registriert das Interface IWorkerTicketProvider<MyWorker> auf die Implementierung MyTicketProvider.
Am Ende erhälst Du also MyTicketProvider wenn Du nach IWorkerTicketProvider<MyWorker> fragst. Fragst Du aber in diesem Fall nach MyTicketProvider wirst Du nix bekommen.


builder.Services.AddSingleton<MyServiceRunner<MyWorker,MyTicketProvider>>();

registriert jedoch nur MyServiceRunner<MyWorker,MyTicketProvider>, weswegen Du auch nach nichts anderem Fragen kannst.

S
Sandmann Themenstarter:in
8 Beiträge seit 2023
vor einem Jahr

Hey und guten Morgen,

vielen Dank für Eure Hinweise. Youtube als Doku-Quelle hatte ich jetzt ehrlich gesagt so gar nicht auf der Liste. Das empfohlene Video schau ich mir auf jeden Fall mal an.

Nach ein wenig nachdenken über Eure Worte, bin ich jetzt bei dem hier angelangt:


        builder.Services.AddTransient<IWorker, MyWorker>();
        builder.Services.AddSingleton<IWorkerTicketProvider<IWorker>, MyTicketProvider>();
        builder.Services.AddHostedService<MyServiceRunner<IWorker, MyTicketProvider>>();

Das bildet auch logisch konsistent das ab, was ich machen wollte: Es gibt eine konkrete Implementierung eines Service Runners. Der Service Runner arbeitet mit unterschiedlichen Implementierungen von Ticket-Providern und Workern zusammen.

Gruß Jens