Payment system migration 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&lt;BillingId&gt; 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&lt;&gt;).MakeGenericType(Type.GetType(parts[0])),
            parts[1]);
    }

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

public class BillingId&lt;TAdapter&gt; : BillingId 
    where TAdapter : IPaymentAdapter
{
    public BillingId(string value) : base(value) { }
    public override Type Adapter =&gt; 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) =&gt; Provider = provider;
    IServiceProvider Provider { get; }

    public async Task&lt;BillingId&gt; RegisterAsync(CreditCard card) =&gt;
        await Adapter().RegisterAsync(card);

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

    IPaymentAdapter Adapter() =&gt; Adapter(typeof(IMainPaymentAdapter));
    IPaymentAdapter Adapter(BillingId id) =&gt; Adapter(id.Adapter);
    IPaymentAdapter Adapter(Type type) =&gt; 
        (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&lt;BillingId&gt; RegisterAsync(CreditCard card) =&gt;
        new BillingId&lt;IStripeAdapter&gt;(await Client.RegisterAsync(card));

    public async Task ChargeAsync(BillingId id, decimal amount) =&gt;
        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?
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&lt;BillingId&gt; 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) =&gt;
        IsNullOrWhiteSpace(text) ? null :
        text.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries) is string[] p &amp;&amp; p.Length == 2
            ? new BillingId(p[0], p[1]) 
            : throw new FormatException();

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

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

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


So payment gateway looks like:

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

    IPaymentProvider MainProvider { get; }
    IReadOnlyDictionary&lt;string, IPaymentProvider&gt; Providers { get; }

    public async Task&lt;BillingId&gt; RegisterAsync(CreditCard card) =&gt;
        await MainProvider.RegisterAsync(card);

    public async Task ChargeAsync(BillingId id, decimal amount) =&gt;
        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 =&gt; "Stripe";

    public async Task&lt;BillingId&gt; RegisterAsync(CreditCard card) =&gt;
        new BillingId(Name, await Client.RegisterAsync(card));

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