.NetCore IOptions configure object with constructor

Viewed 241

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" : "my_name@mydomain.com"
}

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
}
2 Answers

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>.

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": "my_name@mydomain.com"
  }

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>>();
});
Related