Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register service type + implementation type in Jab modules #158

Open
profix898 opened this issue Dec 16, 2023 · 5 comments
Open

Register service type + implementation type in Jab modules #158

profix898 opened this issue Dec 16, 2023 · 5 comments

Comments

@profix898
Copy link

profix898 commented Dec 16, 2023

I'm having issues with modules in combination with registering both service type and implementation type. In #97 a very similar issue is raised (proposed solution is to change Jab module interfaces to be classes). In #133 it is suggested to change attributes to allow both service+implementation type registration.

But let me try to explain the scenario step by step

For some implementations I would like to register a service type (mostly an interface) and additionally the concrete (implementation) type with the container. This is currently not possible, the only option you have is to put multiple attributes (one for each type or interface to be registered) like this:

[Singleton(typeof(MyImpl))]
[Singleton(typeof(IMyService), typeof(MyImpl))]
public partial class Container { }

However, in that case the singleton does not apply anymore (same for scoped). Because both registrations are singletons, you end up with two instances in the end. This can be solved by using Instance= or Factory= parameters with some custom logic to ensure that only a single instance is ever created. This case is also discussed in #97.
This is not only tedious to write repeatedly, but the custom logic requires a class, because you either need a field to store the instance for Factory= or access to GetService<>() which is only available on the container class. You could (as one example) have something like this:

[Singleton(typeof(MyImpl))]
[Singleton(typeof(IMyService), Factory = nameof(MyImplFactory))]
public partial class Container {
  IMyService MyImplFactory() => GetService<MyImpl>();
}

Here, MyImpl is created by the container and IMyService is "redirected" to point to the same instance.
However, you can't do the same in a Jab module, because Jab modules are interfaces (and you can't implement the required logic).

It seems that there are two potential solutions:

  1. Change Jab modules to be classes (instead of interfaces), this is discussed and suggested here: Support class modules and Import(Type, string Factory) #97
  2. Provide addition registration attributes to allow an implementation type to be registered along with one or more service type (it implements). A less intrusive solution IMO. This is discussed and suggested here: Register both as service and implementation #133

I think the registration attribute could be extended to provide:

  1. a parameter in the registration attribute to (additionally) register the concrete implementation
  2. the ability to specify multiple service types (interfaces) for the same implementation

This might look like this:

[Singleton(typeof(ITypeA), typeof(ITypeB), typeof(MyClass), registerInstance: true)]
// or in generic attribute syntax
[Singleton<ITypeA, ITypeB, MyClass>(registerInstance: true)]

Since all "elements" are registered together in that case, the source generator would "know" to produce a proper singleton for MyClass which gets exposed (resolvable via the container) as ITypeA and ITypeB (and if registerInstance=true also as MyClass itself).

From my POV this would solve both issues quite elegantly. What do you think?

I'm open to suggestions on how to combine Jab modules and multiple registrations for a single implementation (even if it requires a little more code), because I don't think that Jab can be used for the outlined scenario above at the moment.

@pakrym
Copy link
Owner

pakrym commented Dec 16, 2023

I agree that supporting multiple implementations with the same service is a more elegant solution, but unfortunately, the generator internals are pretty tied to the idea of one service having one implementation; it might take a while to untangle.

Depending on your target framework you can use static interface members to add factories to modules, but I think allowing static class modules might be safe enough.

@profix898
Copy link
Author

profix898 commented Dec 17, 2023

Yes, the fixed one-to-one relationship is probably the most serious shortcoming (for my purpose) in an otherwise awesome library :)

I thought a little about static interface members and default implementation. However, I don't see how you could use static members for this particular problem. Yes, it allows you to implement a factory on the interface, but you still don't have a place to store the singleton instance. Do you have an example, how you would imagine this to work?

Alternatively, I came up with the following design. I realized that the Jab source generator actually implements the following (known) interfaces: IServiceProvider, IServiceScopeFactory, IDisposable, IAsyncDisposable.
This allow me to make my module implement IServiceProvider, and this in turn makes the GetService() method available in the default interface implementation of the module (full example in Program.txt).

[ServiceProviderModule]
[Scoped(typeof(ITypeA), Factory=nameof(TypeAFactory))]
[Scoped(typeof(ITypeB), Factory=nameof(TypeBFactory))]
[Scoped(typeof(MyClass))]
public interface IModuleA : IServiceProvider
{
    ITypeA TypeAFactory() => (ITypeA) GetService(typeof(MyClass));

    ITypeB TypeBFactory() => (ITypeB) GetService(typeof(MyClass));
}

Unfortunatly, the source generator fails to implement the factory methods in the generated/embedded Scope class. So this doesn't work either. Otherwise it would be a very reasonable workaround.

@profix898
Copy link
Author

profix898 commented Dec 19, 2023

Ok, I figured it out. You have to use a static factory method, which takes an IServiceProvider argument (used to pass the container to the factory). The example from the previous comment then looks like this:

[ServiceProviderModule]
[Singleton(typeof(ITypeA), Factory = nameof(TypeAFactory))]
[Singleton(typeof(ITypeB), Factory = nameof(TypeBFactory))]
[Singleton(typeof(MyClass))]
public interface IModuleA : IServiceProvider
{
    public static ITypeA TypeAFactory(IServiceProvider sp) => (ITypeA) sp.GetService(typeof(MyClass));

    public static ITypeB TypeBFactory(IServiceProvider sp) => (ITypeB) sp.GetService(typeof(MyClass));
}

Interface inheritance is optional, but IMO improves readability/traceability of the hierarchy.
This way you can actually use multiple registrations (per instance) with singleton/scoped in a Jab module. Nice :)

(full example in Program2.txt)

@pakrym
Copy link
Owner

pakrym commented Dec 20, 2023

You can simplify it even more by using factory parameter injection:

    [ServiceProviderModule]
    [Singleton(typeof(ServiceDefinedInAModule))]
    [Singleton(typeof(IInterface1), Factory = "Create1")]
    [Singleton(typeof(IInterface2), Factory = "Create2")]
    public interface IModule
    {
        public static IInterface1 Create1(ServiceDefinedInAModule s) => s;
        public static IInterface2 Create2(ServiceDefinedInAModule s) => s;
    }

@profix898
Copy link
Author

@pakrym: Thanks, that's obviously even better! Seems that "factory parameter injection" is an undocumented feature (not mentioned here: https://github.com/pakrym/jab#factories), which greatly simplifies a couple of scenarios for me. "Redirecting" alternative registrations being the most important.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants