soxtoby / SlackNet

A comprehensive Slack API client for .NET

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Microsoft DI, add overload with IServiceProvider?

sommmen opened this issue · comments

Hiya,

I have some settings object coming from json:

public interface ISlackNotificationSettings
{
    string? Token { get; }
}

public class SlackNotificationSettings : ISlackNotificationSettings
{
    /// <inheritdoc />
    public string? Token { get; set; }
}
  "SlackNotification": {
      "Token": "${secrets:SlackNotification:Token}",
  },
services.Configure<SlackNotificationSettings>(_configuration.GetSection("SlackNotification"));
services.AddSingleton<ISlackNotificationSettings>(p => p.GetRequiredService<IOptions<SlackNotificationSettings>>().Value);

Can we get an overload for AddSlackNet that also gives the IServiceProvider? because now i have to wire this up in a dirty way by first providing a temporary service provider like so;

 using var provider = services.BuildServiceProvider();
 var settings = provider.GetRequiredService<ISlackNotificationSettings>();
 services.AddSlackNet(c =>
 {
     c.UseApiToken(settings.Token);
 });

Whereas i'd like to write;

 services.AddSlackNet((c,p) =>
 {
     c.UseApiToken(p..GetRequiredService<ISlackNotificationSettings>().Token);
 });

For better or worse, there's no way for SlackNet to provide such a method without resorting to a temporary service provider as above, since most of the configuration methods called inside AddSlackNet are themselves registering services in the service collection.

I'm not sure that leaning on the service provider is providing much benefit here; I'd recommend reading the configuration up-front instead. The code would look something like this:

var slackNotificationSettings = _configuration.GetSection("SlackNotification").Get<SlackNotificationSettings>();
services.AddSlackNet(c => c.UseApiToken(slackNotificationSettings.Token));

If you need to inject the settings elsewhere, you can still register them with:

services.AddSingleton(slackNotificationSettings);

The advantages of this approach are simpler code overall, and finding out about any deserialization issues straight away instead of when IOptions<T>.Value is accessed during dependency resolution, which can lead to some pretty gnarly stack traces.

Hope that helps.

I'm not sure that leaning on the service provider is providing much benefit here; I'd recommend reading the configuration up-front instead. The code would look something like this:

I have the following setup where the settings are defined in an executable project, the idea was to inject interfaces into the classes and not be coupled to IOptions (not a hard requirement, just how i have it setup now). I'm not sure how your suggestions would apply here exactly?

Startup

// Other settings
services.Configure<SlackNotificationSettings>(_configuration.GetSection("SlackNotification"));
services.AddSingleton<ISlackNotificationSettings>(p => p.GetRequiredService<IOptions<SlackNotificationSettings>>().Value);

// Other services
services.AddNotificationCentre();

Lib

public static IServiceCollection AddNotificationCentre(this IServiceCollection services)
{
    services.AddEfNotificationStore();

    services.AddSlackNotificationPusher();
    services.AddEmailNotificationPusher();
    services.AddMSTeamsNotificationPusher();

    services.AddNotificationCentreCore();

    return services;
}

public static IServiceCollection AddSlackNotificationPusher(this IServiceCollection services)
{
    // TODO Remove temp. SP. See: https://github.com/soxtoby/SlackNet/issues/202
    using var provider = services.BuildServiceProvider();
    var settings = provider.GetRequiredService<ISlackNotificationSettings>();
    services.AddSlackNet(c =>
    {
        c.UseApiToken(settings.Token);
    });
    services.AddSingleton<INotificationPusherManager, SlackNotificationPusherManager>();

    return services;
}

The advantages of this approach are simpler code overall, and finding out about any deserialization issues straight away instead of when IOptions.Value is accessed during dependency resolution, which can lead to some pretty gnarly stack traces.

You're right and that sucks, however i prefer not to crash my entire app because a single settings is missing. My app has a lot of hangfire jobs doing various background tasks, so if this particular job fails, that is fine and i can go deal with it.

tbh i thought this was easy to do, given its what httpclient does. I'll think on this some more.

https://github.com/sommmen/lightr/blob/992bb39701fbc9ca7e549a1650ef8e0a2a8f2658/src/Lightr/Lightr/DI/ServiceCollectionExtensions.cs#L34

Hmm I think I understand better now - I'd assumed SlackNet was being added in the application code where the configuration was being read.

I can think of two alternatives. The first is to skip UseApiToken and instead provide the token inside the class that uses it.

e.g.

class SlackNotificationPusherManager(
    ISlackApiClient slack,
    IOptions<SlackNotificationSettings> options
) : INotificationPusherManager
{
    public Task PushNotification(Notification notification)
    {
        if (options.Value.Token is string token)
        {
            await slack.WithAccessToken(token).Chat.PostMessage(...);
        }
    }
}

You could also pass the token to the client in the constructor and hold onto the result (WithAccessToken returns a new client object rather than changing the original one).

The other alternative is to specify how the Slack client is constructed inside AddSlackNet:

services.AddSlackNet(c => c
    .UseApiClient(p => new SlackApiClient(
        p.GetRequiredService<IHttp>(),
        p.GetRequiredService<ISlackUrlBuilder>(),
        p.GetRequiredService<SlackJsonSettings>(),
        p.GetRequiredService<IOptions<SlackNotificationSettings>>().Value.Token))
);

This would ensure the client is configured wherever it's injected, but it's not so easy to validate the settings this way, and will break if the SlackApiClient constructor ever changes.

I took the first option, thanks for your time!