Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fluent API design in C# with nested generic constraints

I'm currently implementing an IoC setup API for internal use (heavily inspired to Autofac's module system).

We have Modules which are configurable via a strongly typed configuration, and I want a module to be able to require other modules, so I can have a "composition-root"-like main module, that is going to bootstrap the whole application.

public interface IModule<TConfig>
{
    TConfig Config { get; }

    void Load(ContainerBuilder builder);

    void LoadExtraModules(ModuleRegister register);
}

I'm currently designing the ModuleRegisterclass. What I want to be able to do is similar to this:

public class MyModule : ModuleBase<ApplicationConfiguration>
{
    public void LoadExtraModules(ModuleRegister register)
    {
        register.Module<SqlModule>().WithConfig(new SqlConfiguration() { ... });
    }
}

public class SqlModule : ModuleBase<SqlConfiguration>
{
    public void Load(ContainerBuilder builder)
    {
         // configuration code.
    }
}

What I would like is to have Intellisense somehow suggest that SqlConfiguration is the right configuration type for SqlModule, but I'm failing to do that: I would like to express a type parameter akin to

// ... inside an helper ExtraModulesRegister<TModule> class

public void WithConfig<TConfig>(TConfig configuration)
    where TModule : IModule<TConfig>
{
    ...
}

but obviously I can only express constraints to TConfig, not to TModule.

The only solution I found is to use an Extension Method like the following:

    public static void WithConfig<TConfig, TModule>(this ExtraModulesRegister<TModule> register,
        TConfig configuration)
        where TModule : IModule<TConfig>, new()
    {
        register.LoadModule<TModule, TConfig>(configuration);
    }

so I can express two type constraints, one of which on the already defined generic parameter TModule.

I can (almost) freely change the design of everything.

Any suggestion is appreciated.

like image 988
A. Chiesa Avatar asked Jan 01 '26 22:01

A. Chiesa


1 Answers

I tried to parametrize with both parameters the ExtraModulesRegister class itself:

public class ExtraModulesRegister<TModule, TConfig> wher TModule : IModule<TConfig> {

    void WithConfig(TConfig config) {

    }

}

But now you may need some trick let the TConfig get inferred from SqlConfig, so you dont need to pass both parameters. I guess something like helper type can help, so you call register.Module(X<SqlModule>()) so by passing parameter of something like X<TModule> makes Module() method infer both, TModule and TConfig.

public ExtraModulesRegister<TModule, TConfig> Module<TModule, TConfig>(X<TModule> module) where TModule : IModule<TConfig> {
    ...
}

class X<T> {}

public static X<T> X<T>() {
    return new X<T>();
}

Unfortunately it looks like C# is not able to infer the types. In java the same pattern works, and compiler can infer both types from one argument and defined relation between them.

Edit: This works in C#, but it`s maybe not the syntax, you'd like:

public class ExtraModulesRegister<TConfig> {
    void WithConfig(TConfig config) {}
}

// Module method
public ExtraModulesRegister<TConfig> Module<TConfig>(IModule<TConfig> fakeModule) {
    Return new ExtraModulesRegister<TConfig>();
}

// Usage
register.Module(default(SqlModule)).WithConfig(new SqlConfig()); 
like image 197
Ondřej Fischer Avatar answered Jan 03 '26 12:01

Ondřej Fischer



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!