5
\$\begingroup\$

GitHub

There need to be a software design supporting gradual migration from one payment system (E-xact) to another (Stripe). Credit cards data are stored outside, so I would like to use both systems in parallel for a while to do not make all users to re-enter credit card data.

Here is what I have at the moment in the domain core:

public interface IMainPaymentAdapter : IPaymentAdapter
{
}

public interface IPaymentAdapter : IPaymentService
{
}

public interface IPaymentGateway : IPaymentService
{
}

public interface IPaymentService
{
    Task<BillingId> RegisterAsync(CreditCard card);
    Task ChargeAsync(BillingId id, decimal amount);
}

The idea is that IPaymentService defines API shape, IPaymentAdaptor is about to be implemented for each payment system in use, IPaymentGateway dispatches calls to the right adaptor based on a BillingId and is supposed to be injected into the consuming code:

public abstract class BillingId
{
    public static BillingId Parse(string text)
    {
        var parts = text.Split('/');
        return (BillingId)Activator.CreateInstance(
            typeof(BillingId<>).MakeGenericType(Type.GetType(parts[0])),
            parts[1]);
    }

    protected BillingId(string value) => Value = value;
    public string Value { get; }
    public abstract Type Adapter { get; }
    public override string ToString() => 
        $"{Adapter.AssemblyQualifiedName}/{Value}";
}

public class BillingId<TAdapter> : BillingId 
    where TAdapter : IPaymentAdapter
{
    public BillingId(string value) : base(value) { }
    public override Type Adapter => typeof(TAdapter);
}

My gateway looks like this – note that card registration call is always dispatched to the current IMainPaymentAdaptor implementation, while everything else goes to the adaptor that provided the billing id originally:

[Service]
public class PaymentGateway : IPaymentGateway
{
    public PaymentGateway(IServiceProvider provider) => Provider = provider;
    IServiceProvider Provider { get; }

    public async Task<BillingId> RegisterAsync(CreditCard card) =>
        await Adapter().RegisterAsync(card);

    public async Task ChargeAsync(BillingId id, decimal amount) =>
        await Adapter(id).ChargeAsync(id, amount); 

    IPaymentAdapter Adapter() => Adapter(typeof(IMainPaymentAdapter));
    IPaymentAdapter Adapter(BillingId id) => Adapter(id.Adapter);
    IPaymentAdapter Adapter(Type type) => 
        (IPaymentAdapter)Provider.GetService(type);
}

Here is what I have in a Stripe adapter assembly:

public interface IStripeAdapter : IMainPaymentAdapter
{
}

[Service]
public class StripeAdapter : IStripeAdapter
{
    StripeClient Client { get; } = new StripeClient();

    public async Task<BillingId> RegisterAsync(CreditCard card) =>
        new BillingId<IStripeAdapter>(await Client.RegisterAsync(card));

    public async Task ChargeAsync(BillingId id, decimal amount) =>
        await Client.Charge(id.Value, amount);
}

Note that E-xact adapter (a decommissioning one) should be based on IPaymentAdaptor, not the IMainPaymentAdapter:

public interface IEXactAdapter : IPaymentAdapter
{
}

What would you say about this design approach? Does it look clean enough?

\$\endgroup\$

1 Answer 1

-1
\$\begingroup\$

Here is a simplified version, thanks to this answer.

public interface IMainPaymentProvider : IPaymentProvider
{
}

public interface IPaymentProvider : IPaymentService
{
    string Name { get; }
}

public interface IPaymentGateway : IPaymentService
{
}

public interface IPaymentService
{
    Task<BillingId> RegisterAsync(CreditCard card);
    Task ChargeAsync(BillingId id, decimal amount);
}

Where:

[TypeConverter(typeof(BillingIdTypeConverter))]
[JsonConverter(typeof(BillingIdJsonConverter))]
public class BillingId
{
    public static BillingId Parse(string text) =>
        IsNullOrWhiteSpace(text) ? null :
        text.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries) is string[] p && p.Length == 2
            ? new BillingId(p[0], p[1]) 
            : throw new FormatException();

    public BillingId(string provider, string value) => 
        (Provider, Value) = (provider, value);

    public string Provider { get; }
    public string Value { get; }

    public override string ToString() => 
        $"{Provider}:{Value}";
}

So payment gateway looks like:

[Service]
public class PaymentGateway : IPaymentGateway
{
    public PaymentGateway(IMainPaymentProvider mainProvider, params IPaymentProvider[] providers) => 
        (MainProvider, Providers) = 
        (mainProvider, providers.ToDictionary(p => p.Name));

    IPaymentProvider MainProvider { get; }
    IReadOnlyDictionary<string, IPaymentProvider> Providers { get; }

    public async Task<BillingId> RegisterAsync(CreditCard card) =>
        await MainProvider.RegisterAsync(card);

    public async Task ChargeAsync(BillingId id, decimal amount) =>
        await Providers[id.Provider].ChargeAsync(id, amount); 
}

And a sample provider could be:

public interface IStripeProvider : IMainPaymentProvider
{
}

[Service]
public class StripeProvider : IStripeProvider
{
    StripeClient Client { get; } = new StripeClient();

    public string Name => "Stripe";

    public async Task<BillingId> RegisterAsync(CreditCard card) =>
        new BillingId(Name, await Client.RegisterAsync(card));

    public async Task ChargeAsync(BillingId id, decimal amount) =>
        await Client.Charge(id.Value, amount);
}
\$\endgroup\$
1
  • 6
    \$\begingroup\$ Thanks for posting this code. It's a good idea to summarise which changes you made, and why - a self-answer ought to review the code, just like any other answer. \$\endgroup\$ Mar 10, 2022 at 16:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.