18

I have two classes PaymentGatewayFoo, PaymentGatewayBoo that both implements a common interface of IPaymentGateway:

interface IPaymentGateway { }

class PaymentGatewayFoo : IPaymentGateway { }
class PaymentGatewayBoo : IPaymentGateway { }

The client side request has an identifier who's response to determine which implementation to use:

public class OrderService
{
    private readonly IPaymentGateway _service;

    public void DoOrder(bool isFoo)
    {
        if (isFoo)
            //this._service should be resolved with PaymentGatewayFoo
        else
            //this._service should be resolved with PaymentGatewayBoo

        this._service.Pay();
    }
}

How do I resolve the proper implementation based on the client's request on run-time?

This question is not duplicate, its similar but its about two separate controllers (Even the answers suggested that the code didn't even needed the conditional dependency injection), in my case the conditional dependency is needed on run-time based on a client property value.

Shahar Shokrani
  • 7,598
  • 9
  • 48
  • 91
  • 1
    You cannot use conditional injection here because the type of the dependency will be determined at runtime and can change with every `DoOrder` call. So you need to inject both implementations. – dymanoid Sep 02 '19 at 14:21
  • 1
    It's not a dependency injection problem. Looks like you need the [Factory Design Pattern](https://refactoring.guru/design-patterns/factory-method). Take a look – Diego Rafael Souza Sep 02 '19 at 14:27

3 Answers3

22

There are several options here, but the two that to me are the most obvious are using a factory or the adapter pattern.

1.1 Use a factory

public class OrderService
{
    private readonly IPaymentGatewayFactory _factory;

    public void DoOrder(bool isFoo)
    {
        IPaymentGateway service = _factory.Create(isFoo);
        service.Pay();
    }
}

Where the factory can be:

public class PaymentGatewayFactory : IPaymentGatewayFactory 
{
    public PaymentGatewayFactory(PaymentGatewayFoo foo, PaymentGatewayBoo boo) {...}

    public IPaymentGateway Create(bool isFoo) =>
        isFoo ? this.foo : this.boo;
}

Pros and cons:

1.2 Factory variation

When the list of IPaymentGateway implementations grows or is dynamic, the injection of a fixed list of payment implementations in the PaymentGatewayFactory can be unsuitable. Alternatively, you can inject a collection of IPaymentGateway implementations into the factory and pick the proper implementation based provided information. For instance:

public class PaymentGatewayFactory : IPaymentGatewayFactory 
{
    public PaymentGatewayFactory(IEnumerable<IPaymentGateway> gateways) {...}

    public IPaymentGateway Create(string paymentType) =>
        this.gateways.Single(gateway => gateway.Type == paymentType);
}

In this design, instead of having a two-state isFoo boolean, there's a paymentType, that can contain many values (e.g. Paypal, Card, BankTransfer, etc). For the factory to deal with this dynamic scenario it needs to match the provided payment type with the injected IPaymentGateway instances. Here I choose to add an string Type property to the IPaymentGateway interface that all implementations must implement. In your case:

class PaymentGatewayFoo : IPaymentGateway { public string Type => "Foo"; }
class PaymentGatewayBoo : IPaymentGateway { public string Type => "Boo"; }

2. Use an adapter

public class OrderService
{
    private readonly IPaymentGateway gateway;

    public void DoOrder(bool isFoo)
    {
        this.gateway.Pay(isFoo);
    }
}

Where the adapter can be:

public class PaymentGatewayAdapter : IPaymentGateway
{
    public PaymentGatewayAdapter(PaymentGatewayFoo foo, PaymentGatewayBoo boo) {...}

    public void Pay(bool isFoo)
    {
        var service = isFoo ? this.foo : this.boo;

        service.Pay(isFoo);
    }
}

Pros and cons:

  • Advantage of this is that the client only needs to be aware of a single abstraction.
  • Downside it that this only works when it reasonable to supply the isFoo runtime data as method argument to the IPaymentGateway implementations. You'll often find that the information sent to the Pay method contains more information, including the part that you want to apply the condition to, but this might not always be the case. If it's not the case, adding this extra piece of information can pollute the IPaymentGateway abstraction.
  • With the adapter implementation you could make the same variation as shown above with the factory where you have more flexability, but the downside of having to pass on extra runtime data that might be unnatural to the abstraction will remain.

Alternative implementations

As you noticed, in my factory and adapter, the implementations are injected directly. Not even by their abstractions, but by their concrete types. This might seem strange, but doing so is completely fine as long as the adapter and factory are part of the application's entry point (a.k.a. the Composition Root).

But other, more dynamic options can be used, such as:

  • Injecting a Func<PaymentType, IPaymentGateway> delegate to resolve the types.
  • Injecting a Dictionary<PaymentType, IPaymentGateway>.
  • Injecting a collection of IPaymentGateway implementations.
  • Injecting the container itself
  • Using dynamic filtering, as Armand suggests, but do note that is causes you to add the Identifier property to the interface, where it only exists for technical reasons. No consumer (other than the adapter or the factory) is interested in this identifier. It, therefore, doesn't belong to the interface. A better solution is to solve this problem in the Composition Root, possibly by marking the implementations with an attribute, for instance.
crsmn
  • 15
  • 7
Steven
  • 166,672
  • 24
  • 332
  • 435
13

Edit: .net now supports numerable injection.


After trying to implement the great answers here, it looks like that the native DI .net core container does not support injecting of numerables (like IPaymentGateway[] as @armand suggested) so I ended up with a delegate resolver based on switch case (not on reflection in order to save performance):

startup.cs

services.AddTransient<PaymentGatewayResolver>(serviceProvider => key =>
{
    switch (key)
    {
        case E_PaymentGatewayType.Foo:
            return serviceProvider.GetService<PaymentGatewayFoo>();
        case E_PaymentGatewayType.Boo:
            return serviceProvider.GetService<PaymentGatewayBoo>();
        case E_PaymentGatewayType.Undefined:
            default:
            throw new NotSupportedException($"PaymentGatewayRepositoryResolver, key: {key}");
    }
});

The delegate:

public delegate IPaymentGateway PaymentGatewayResolver(E_PaymentGatewayType paymentGatewayType);

The client order service:

private readonly PaymentGatewayResolver _paymentGatewayResolver;

public OrderService(PaymentGatewayResolver paymentGatewayResolver)
{
    this._paymentGatewayResolver = paymentGatewayResolver; 
}

public DoOrder(E_PaymentGatewayType paymentGatewayType)
{
    IPaymentGateway paymentGateway = this._paymentGatewayResolver(paymentGatewayType);
    paymentGateway.Pay();
}
Shahar Shokrani
  • 7,598
  • 9
  • 48
  • 91
3

This is how I approach this type of problem where I have cases that needs different implementations.

public abstract class PaymentGateway : IPaymentGateway 
{
    public long Identifier {get;}
    ...
}

public class PaymentGatewayFoo : PaymentGateway
{
    public long Identifier => 1;
    ...
}

public class PaymentGatewayBoo : PaymentGateway
{
    public long Identifier => 2;
    ...
}

public class PaymentGatewayProvider
{
    private IPaymentGateway[] gateways;
    public PaymentGatewayProvider(IPaymentGateway[] gateways)
    {
         this.gateways = gateways;
    }

    public IPaymentGateway GetGateForClient(bool f) //This can be anything that you can use to identify which payment gateway you need
    {
         //As an example I would usually pass in a client or something that I can then use to identify which payment provider is mapped to a certain client, this way you can have hundreds of payment providers, but in your case you just had a boolean, so I used that
         if(f)
              return gateways.First(f=> f.Identifier == 1);
         return gateways.First(f=> f.Identifier != 1);
    }
}


public class OrderService
{
    private readonly PaymentGatewayProvider _provider;

    public void DoOrder(bool isFoo)
    {
        var service = _provider.GetGateForClient(isFoo);
        this._service.Pay();
    }
}

The way you identify payment providers can be anything, I just used longs as an example here, but you can map it to an enum and rather use that to do identification, then you pass the enum to the GetGateForClient method to get the correct payment gateway

This might not compile out of the box, but should give you a general idea of how to solve the problem.

Armand
  • 9,847
  • 9
  • 42
  • 75
  • Hey, thanks. 1. How to register the `PaymentGatewayProvider` inside startup.cs? 2. is the `PaymentGateway` should be also registered with `IPaymentGateway`? – Shahar Shokrani Sep 02 '19 at 14:27
  • 1
    You should be able to register `PaymentGatewayProvider` to itself in the startup file, sorry for only replying now, didn't see the notification for this @ShaharShokrani – Armand Sep 09 '19 at 15:09
  • 2
    I got a resolve error. After that I just use IEnumarable instead of Array [] – mperk Dec 30 '20 at 18:08