SimonAx
SimonAx

Reputation: 1378

.NetCore IOptions configure object with constructor

In a .NetCore application I want to inject into a service a setting that has some custom logic in the constructor. In particular, I have a setting section looking similar to this:

"Email": {
    "FromEmailName" : "My name",
    "FromEmail" : "[email protected]"
}

Now, I'd like to bind this to a class looking like:

public class EmailSettings {
    public System.Net.Mail.MailAddress MailAddress { get; } 
 
    public EmailSetting(string fromEmailName, string fromEmail) {
        MailAddress = new MailAddress(fromEmail, fromEmailName);
    }
}

From the docs I see that the EmailSettings class must have a parameterless constructor, but is there any way that I can overcome this problem by introducing an 'intermediate' class UnparsedEmailSetting, and then do something like the code below?

public static void BindEmailConfig(this IServiceCollection services, IConfiguration configuration) {
    var section = configuration.GetSection("Email");
    services.Configure<UnparsedEmailSettings>(section);
    var emailConfig = section.Get<UnparsedEmailSettings>();
    var emailSettings = new 
    EmailSettings(emailConfig.FromEmailName, emailConfig.FromEmail);
    services.Configure<EmailSettings>(emailSetting); // does not work
    services.RemoveConfiguration<UnparsedEmailSettings); // method does not exist
}

Upvotes: 4

Views: 3450

Answers (2)

smoksnes
smoksnes

Reputation: 10851

First, the answer from Jakob will work fine, but it won't give you the services that IOptions can provide (such as reloading). If that is important and the information in your question is complete you should be able to do a work-around. Otherwise I strongly suggest Jakobs approach.

Keep the settings:

  "Email": {
    "FromEmailName": "My name",
    "FromEmail": "[email protected]"
  }

Register your config like so:

services.Configure<EmailSettings>(Configuration.GetSection("Email"));

And change your model:

public class EmailSettings
{
    public string FromEmailName { get; init; }
    public string FromEmail { get; init; }

    public System.Net.Mail.MailAddress MailAddress => new MailAddress(FromEmail, FromEmailName);
}

That way you don't need any special logic in your constructor. In C#10 C#11 you should be able to add required to the property as well.

public required string FromEmail { get; init; }

The downside is that your new EmailSettings will expose more members than necessary. But it will still be immutable.

If it's important to also not expose any other members you can simply add an interface:

public interface IEmailSettings
{
    System.Net.Mail.MailAddress MailAddress { get; }
}

And change your registration like so:

services.Configure<EmailSettings>(Configuration.GetSection("Email"));
services.AddSingleton<IOptions<IEmailSettings>>(sp =>
    sp.GetRequiredService<IOptions<EmailSettings>>());

Now you can inject IOptions<IEmailSettings>. Or you can change the registration to IOptionsMonitor if you want to enable reloading.:

services.AddSingleton<IOptionsMonitor<IEmailSettings>>(sp =>
{
    return sp.GetRequiredService<IOptionsMonitor<EmailSettings>>();
});

Upvotes: 3

Jakob
Jakob

Reputation: 1216

Since adding an option basically means adding a singleton of type IOptions<TOptions>, you can do that. There is no constructor for Options, but you can use the create method from the Options extensions.

services.AddSingleton<IOptions<EmailSetting>>(serviceProvider =>
{
    return Options.Create(new EmailSetting(Configuration.GetSection("Email").GetSection("FromEmailName").Value,
                    Configuration.GetSection("Email").GetSection("FromEmail").Value));
});

It can then be injected as usual with IOptions<EmailSetting>.

Upvotes: 4

Related Questions