Extensions: Should IStringLocalizer be covariant?

Created on 19 Jan 2019  路  12Comments  路  Source: dotnet/extensions

Like ILogger<T>, IOptions<T> or IOptionsMonitor<T>, should IStringLocalizer<T> be covariant? This would allow this kind of pattern:

public class Base
{
    public Base(IStringLocalizer<Base> localizer) { }
}
public class Derived : Base
{
    public Base(IStringLocalizer<Derived> localizer)
        : base(localizer) { }
}
3 - Done area-localization XS enhancement

Most helpful comment

Excellent, I'll send a PR soon, then 馃槃

All 12 comments

@Eilon did you guys have a chance to discuss this change? If it's accepted, I'd love to submit a PR as I plan to use the localization stack in my OpenIddict project.

Tagging @davidfowl and @mkArtakMSFT

I assume it's not a breaking change to add out?

I do like this change, and it's certainly nice in logging.

I assume it's not a breaking change to add out?

Yep, it's not a breaking change (I just tested to confirm it).

@mkArtakMSFT / @davidfowl / @ryanbrandenburg - any thoughts on this? This sounds good to me.

(BTW Fowler has no power, and RyanB on vaca, so might be slow response right now.)

I'm in a hotel now 馃槃, seems fine, I'm not sure what the use case is though. Do you have something concrete @PinpointTownes ?

Do you have something concrete @PinpointTownes ?

Yeah, the main scenario I have in mind is to allow an application to override the resources location when subclassing a class from a class library that comes with .resx files and uses [assembly: ResourceLocation("...")].

// Defined in a class library.
public class Base
{
    // Uses the resources defined in the class library (ClassLibrary.Resources.Base).
    public Base(IStringLocalizer<Base> localizer) { }
}
// Subclassed in application code.
public class Derived : Base
{
    // Uses the resources defined in the application (App.Resources.Derived).
    // Both this class and the base one will use the application strings.
    public Base(IStringLocalizer<Derived> localizer)
        : base(localizer) { }
}

It's exactly like how options and generic loggers work:

public class Base
{
    public Base(IOptions<Base> options) { }
}
public class Derived : Base
{
    // Resolve specialized Derived options and inject them
    // into the base class as IOptions<Base>.
    public Base(IOptions<Derived> options)
        : base(options) { }
}

It's something I use in OpenIddict: I have a OpenIddictServerOptions class that inherits from OpenIdConnectServerOptions (from ASOS) and defines additional options. Wherever IOptionsMonitor<OpenIdConnectServerOptions> is used, I simply flow IOptionsMonitor<OpenIddictServerOptions> (e.g https://github.com/openiddict/openiddict-core/blob/dev/src/OpenIddict.Server/OpenIddictServerHandler.cs#L25).

LGTM /cc @rynowak @ryanbrandenburg @pranavkm

Excellent, I'll send a PR soon, then 馃槃

Thinking through this now I wonder if the code isn't correct as it is today. You gave some examples of C# code, but lets consider some resx KVPs that might be in those classes.

|Key|Base value|Derived value|
|----|-----------|---------------|
|Greeting|A basic greeting to you!|A specific greeting to you!|

So now IStringLocalizer<Base> base could return either a basic greeting or a specific greeting, which seems wrong to me. @davidfowl thoughts? I feel like making it covariant only makes sense if we actually walk the class inheritance tree as part of class localization, which we do not today (and would be a pretty complicated system if we did).

Talked it over with @danroth27 and decided that what I described was not only fine but exactly what was being asked for. I'll look into the PR.

Thanks @PinpointTownes!

@ryanbrandenburg thanks for merging it! 馃憦

Was this page helpful?
0 / 5 - 0 ratings