C
C#•3y ago
Welles

asp.net core services initialization

Hi there, Was wondering what the best way is to go about initializing services I add to my asp.net builder. The initialization code needs to run async methods, so the constructor is not an option here unless I do some weird stuff. Best regards!
14 Replies
Becquerel
Becquerel•3y ago
if you're able to use those services as singletons, you can construct them manually and pass them to the DI container
var myService = new Service();
await myService.InitializeAsync();

serviceCollection.AddSingleton(myService);
var myService = new Service();
await myService.InitializeAsync();

serviceCollection.AddSingleton(myService);
another option is to use the factory pattern
Welles
WellesOP•3y ago
That's an option as well, however I don't know if it's a good idea to add them a singleton. What I did now is the following, not sure if this a good practice: Program.cs
builder.Services.AddScoped<CardanoService>();
builder.Services.AddHostedService<InitializationService>();
builder.Services.AddScoped<CardanoService>();
builder.Services.AddHostedService<InitializationService>();
InitializationService
public sealed class InitializationService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CardanoService> _logger;

public InitializationService(ILogger<CardanoService> logger,, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}

public async Task StartAsync(CancellationToken token)
{
using (IServiceScope scope = _serviceProvider.CreateScope())
{
CardanoService cardanoService =
scope.ServiceProvider.GetRequiredService<CardanoService>();

await cardanoService.Initialize();
}

}

public async Task StopAsync(CancellationToken token)
{
await Task.Yield();
}
}
public sealed class InitializationService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CardanoService> _logger;

public InitializationService(ILogger<CardanoService> logger,, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}

public async Task StartAsync(CancellationToken token)
{
using (IServiceScope scope = _serviceProvider.CreateScope())
{
CardanoService cardanoService =
scope.ServiceProvider.GetRequiredService<CardanoService>();

await cardanoService.Initialize();
}

}

public async Task StopAsync(CancellationToken token)
{
await Task.Yield();
}
}
Stroniax
Stroniax•3y ago
@Welles I would definitely go with the factory pattern for this. Effectively that means you'll have one interface/class responsible for creating the one you need. Since constructors can't be async, I would recommend a static async factory method on the type you're constructing as well.
public interface ICardanoFactory {
ValueTask<ICardanoService> CreateAsync(CancellationToken cancellationToken);
}
public class DefaultCardanoFactory : ICardanoFactory {
private IDependency _dependency;
public DefaultCardanoFactory(IDependency dependency) => _dependency = dependency;
public async ValueTask<ICardanoService> CreateAsync(CancellationToken cancellationToken) => await CardanoService.CreateAsync(_dependency, cancellationToken);
}

public class CardanoService : ICardanoService {
public static ValueTask<CardanoService> CreateAsync(IDependency dependency, CancellationToken cancellationToken) {
var svc = new CardanoService(dependency);
await svc.InitializeAsync(cancellationToken);
return svc;
}
private CardanoService(IDependency dependency) => _dependency = dependency;
private ValueTask InitializeAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
}
public interface ICardanoFactory {
ValueTask<ICardanoService> CreateAsync(CancellationToken cancellationToken);
}
public class DefaultCardanoFactory : ICardanoFactory {
private IDependency _dependency;
public DefaultCardanoFactory(IDependency dependency) => _dependency = dependency;
public async ValueTask<ICardanoService> CreateAsync(CancellationToken cancellationToken) => await CardanoService.CreateAsync(_dependency, cancellationToken);
}

public class CardanoService : ICardanoService {
public static ValueTask<CardanoService> CreateAsync(IDependency dependency, CancellationToken cancellationToken) {
var svc = new CardanoService(dependency);
await svc.InitializeAsync(cancellationToken);
return svc;
}
private CardanoService(IDependency dependency) => _dependency = dependency;
private ValueTask InitializeAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
}
If you want non-transient behavior, your factory could cache the service it creates and reuse that for later requests.
Welles
WellesOP•3y ago
Ok I'm goign to refactor the code! I'll do some research on this factory pattern Aaaah I see you have helped me alot in that regard haha 😄 the IDependency is a placeholder I'm guessing right?>
Stroniax
Stroniax•3y ago
Yeah, just to show that you can inject whatever dependencies you might need for CardanoService
Welles
WellesOP•3y ago
Ok got it! 😄 Is there difference between valuetask and task? Hmm, I'm a bit list here. So created factory and made the change in the service (not using interfaces for now)
public sealed class CardanoFactory
{
private readonly ILogger<CardanoService> _logger;
private readonly IHostEnvironment _hostingEnv;
private readonly IConfiguration _configuration;
private readonly DataContext _dataContext;
private readonly BlockfrostService _blockfrostService;

public CardanoFactory(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext)
{
_logger = logger;
_hostingEnv = hostingEnv;
_configuration = configuration;
_blockfrostService = blockfrostService;
_dataContext = dataContext;
}

public async Task<CardanoService> CreateAsync(CancellationToken cancellationToken) => await CardanoService.CreateAsync(_logger,_hostingEnv, _blockfrostService,_configuration, _dataContext, cancellationToken);
}
public sealed class CardanoFactory
{
private readonly ILogger<CardanoService> _logger;
private readonly IHostEnvironment _hostingEnv;
private readonly IConfiguration _configuration;
private readonly DataContext _dataContext;
private readonly BlockfrostService _blockfrostService;

public CardanoFactory(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext)
{
_logger = logger;
_hostingEnv = hostingEnv;
_configuration = configuration;
_blockfrostService = blockfrostService;
_dataContext = dataContext;
}

public async Task<CardanoService> CreateAsync(CancellationToken cancellationToken) => await CardanoService.CreateAsync(_logger,_hostingEnv, _blockfrostService,_configuration, _dataContext, cancellationToken);
}
public async static Task<CardanoService> CreateAsync(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext, CancellationToken cancellationToken)
{
var svc = new CardanoService(logger,hostingEnv,blockfrostService,configuration,dataContext);
await svc.Initialize(cancellationToken);
return svc;
}

private CardanoService(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext)
{
_logger = logger;
_hostingEnv = hostingEnv;
_configuration = configuration;
_blockfrostService = blockfrostService;
_dataContext= dataContext;

_coinSelectionService = new CoinSelectionService(new RandomImproveStrategy(), new SingleTokenBundleStrategy());
fundingWallet = _configuration["TGC:FundWallet"];
_logger.LogDebug($"{this.GetType().Name} initialized!");
}
public async static Task<CardanoService> CreateAsync(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext, CancellationToken cancellationToken)
{
var svc = new CardanoService(logger,hostingEnv,blockfrostService,configuration,dataContext);
await svc.Initialize(cancellationToken);
return svc;
}

private CardanoService(ILogger<CardanoService> logger, IHostEnvironment hostingEnv, BlockfrostService blockfrostService, IConfiguration configuration, DataContext dataContext)
{
_logger = logger;
_hostingEnv = hostingEnv;
_configuration = configuration;
_blockfrostService = blockfrostService;
_dataContext= dataContext;

_coinSelectionService = new CoinSelectionService(new RandomImproveStrategy(), new SingleTokenBundleStrategy());
fundingWallet = _configuration["TGC:FundWallet"];
_logger.LogDebug($"{this.GetType().Name} initialized!");
}
Then I just do builder.services.AddScope<CardanoFactory>(); Might be with the latter I'm doing something wrong. the init function is not being called atm K so I just get an error at the moment. Unable to resolve service for type 'tgcDivideRoyalties.Services.CardanoService' while attempting to activate 'tgcDivideRoyalties.Controllers.WalletsController' I'll tinker a bit 🙂
Stroniax
Stroniax•3y ago
Looking at this now...
Welles
WellesOP•3y ago
haha omg, yeah I thinkl I know. I need to inject cardanofactory in the rest of them instead of cardanoservice
Stroniax
Stroniax•3y ago
ValueTask doesn't allocate when the value is immediately available (not-async). Generally it means better performance if you're caching a value, but not a big deal if you chose to prefer Task. I don't see your code for Initialize in here, is it doing anything?
Welles
WellesOP•3y ago
yeah it is 🙂 but I don't see how this could work if I don't inject cardanofactory instead of cardanoservice in my controller. When does the createAsync method gets triggered? On requests? And I was also thinking, if scoped services are default with asp.net core stuff, isn't that like horrible for performance it needs to contruct each time a requests comes in?
Stroniax
Stroniax•3y ago
You should inject CardanoFactory and then when you need to use CardanoService that's when you'll call CardanoFactory.CreateAsync to get your service. This is the main reason you might want to cache the value after it gets created the first time.
public ValueTask<CardanoService> CreateAsync(...args) {
if (_value is null) { _value = new(...args); await _value.Initialize(ct); }
return _value;
}
public ValueTask<CardanoService> CreateAsync(...args) {
if (_value is null) { _value = new(...args); await _value.Initialize(ct); }
return _value;
}
That is exactly correct, you'll inject the factory instead of the service. (Sorry I didn't make that clear.) Constructing a class might be "slow" in computer terms, but it's likely faster than most of the business logic you're going to be dealing with. The general reason for scoped services is that if something goes bad during one run and a class is in an invalid state, that won't cause the next scope to fail also - you get a "clean slate". This will also help if you have a db connection, file handle, etc. that you don't want to keep open all the time when you aren't using it to free up system resources - the scope cleanup will release those resources. If your service doesn't have any state to it, there's no reason not to register it as a Singleton.
Welles
WellesOP•3y ago
Ok gotcha, it's a lot to get my head around. Need to figure out to just not make it all a singleton and then use AddDbContextFactory instead of normal dbcontext which is scoped by default. A lot of things to figure out As I see it now, using the factory pattern, I still need to await Factory.createAsync somewhere. And I can't do that in constructors, so the issue is the same no? the init code needs to run before the server accepts connections
Stroniax
Stroniax•3y ago
What you're solving with the factory pattern in the Single Responsibility Principle - a consumer isn't responsible for creating and initializing your service. Yes, the async method needs to be called. The service provider doesn't work with async types so you will need to call an async method somewhere, but this way you keep the code base cleaner and if you're using your service in more than one place you don't have to duplicate the initialization logic. You could think of your method in somewhat more generic terms like this:
interface IFactory<T> {
ValueTask<T> GetAsync(CancellationToken);
}
interface IFactory<T> {
ValueTask<T> GetAsync(CancellationToken);
}
The service T can be created by the factory, or just provided. You don't have to create something here - in fact, your factory can get T from DI.
class DIFactory<T> {
DIFactory(T value) => _value = value;
T _value;
ValueTask<T> GetAsync(...) => ValueTask.FromResult(_value);
}
class DIFactory<T> {
DIFactory(T value) => _value = value;
T _value;
ValueTask<T> GetAsync(...) => ValueTask.FromResult(_value);
}
What this does is let you get T anywhere you want it, and the initialization logic can by async if it needs to be. If not (such as DIFactory), no problem. If it is, no problem - you're returning an async type. It's effectively solving the problem of being unable to inject an async-generated type in DI, because with DI you don't want to call new DependencyService in some class that is a consumer of DependencyService - that's not dependency injection at all.
Welles
WellesOP•3y ago
Well, thank you very much for the information here. I need to take a look on practical examples. 😄
Want results from more Discord servers?
Add your server