Home > Software engineering >  Blazor, Is this Singleton abuse? (Sharing iframe values)
Blazor, Is this Singleton abuse? (Sharing iframe values)

Time:05-31

First of all, let me explain why I think I need a singleton. I'm integrating several Hosted Checkout payment processors into my Blazor Server application. All of them work as follows;

  • Index.razor has an Iframe that displays a payment processors url.
  • When the customer completes the payment the iframe redirects back to a url specified by my application, PaymentComplete.razor.
    • PaymentComplete.razor uses a scoped service HostedCheckoutService to raise an event to Index.razor containing the payment response.

This is where the problem comes in. PaymentComplete.razor is hosted inside an iframe therefore is treated with a separate scope. Any property changes or events raised by HostedCheckoutService from within PaymentComplete.razor wont be Index.razor. This makes it (nearly?) impossible to merge information from within the iframe to the scope of Index.razor.

What obviously solves this issue is registering HostedCheckoutService as a singleton. Now the problem is that when one client raises an event from PaymentComplete.razor, all clients will have to handle it.

To solve this I created an IndexBase with a unique property named EventTargetId. When the iframe payment is completed, the return url to PaymentComplete.razor will contain the EventTargetId in the query string.

IndexBase.cs

<iframe style="width:100%;height:50vh" src="@HostedCheckoutFrameSrc " frameborder="0" ></iframe> 


public class IndexBase : ComponentBase, IDisposable
{
    [Inject] NavigationManager NavigationManager { get; set; }
    [Inject] HostedCheckoutService HostedCheckoutService { get; set; }
    [Inject] PaymentApi PaymentApi { get; set; }

    public string HostedCheckoutFrameSrc { get; set; }
    public string EventTargetId { get; set; } = Guid.NewGuid().ToString();

    protected override void OnInitialized()
    {
        HostedCheckoutService.OnPaymentComplete  = PaymentComplete;
    }

    public void Dispose()
    {
        HostedCheckoutService.OnPaymentComplete -= PaymentComplete;
    }

    private void PaymentComplete(string eventTargetId, string paymentJson)
    {
        // Hosted checkout iframe has returned a successfull payment.
        // Do something, send order, notification, ect.
    }

    public async Task InitializePayment()
    {
        string returnUrl = NavigationManager.BaseUri   $"/PaymentComplete?eventTargetId={EventTargetId}";
        InitializePaymentResponse response = await PaymentApi.CreatePaymentRequest(returnUrl);
        // Set iframe src property to third party payment providers url.
        // When customer completes third party payment url, the iframe redirects to PaymentComplete.razor (returnUrl).
        HostedCheckoutFrameSrc = PaymentApi.baseUrl   response.PaymentId;
    }
}

PaymentComplete.razor (redirected from third party url, hosted inside iframe)

This page will grab the EventTargetId from the query string and raise an event on our singleton service.

[Inject] NavigationManager NavigationManager { get; set; }
[Inject] PostFormService PostFormService { get; set; }
[Inject] HostedCheckoutService HostedCheckoutService { get; set; }

protected override async Task OnInitializedAsync()
{
    // We face double render problem, but Form values will be null on secord render anyways.
    if (PostFormService.Form != null)
    {
        NavigationManager.TryGetQueryString<string>("eventTargetId", out string eventTargetId);
        string paymentJson = PostFormService.Form?["PaymentResponse"];
        HostedCheckoutService.PaymentCompleted(eventTargetId, paymentJson);
    }
}

In my singleton HostedCheckoutService, I filter out any subscribers using the EventTargetId.

public class HostedCheckoutService 
{
    public event Action<string, string> OnPaymentComplete;

    public void PaymentCompleted(string eventTargetId, string paymentJson)
    {
        // Instead of raising the event for every instance attached to this action
        // only raise the event for the specified target.
        var instance = OnPaymentComplete.GetInvocationList()
                            .Where(d => d.Target is IndexBase && ((IndexBase)d.Target).EventTargetId == eventTargetId)
                            .FirstOrDefault();
        instance?.DynamicInvoke(eventTargetId, paymentJson);
    }
}

Finally, the question! Does this seem like an unacceptable use of singleton events or does anyone have a better approach? Even though every client wont be handling the event, a call to GetInvocationList() would still contain a list of every subscribed class.

Note: Each event subscriber would not actually be a full IndexBase class. It would be a simple payment component (I simplified this example).

CodePudding user response:

My main concern would be scaling on calling all the registered methods on the event.

As I don't know what else HostedCheckoutService does, what about having a singleton PaymentTransactionService that contains a simple collection of Guids against Actions - and probably registration times to operate a timeout system . Index calls a Register method on PaymentTransactionService to register it's Guid and it's Action. - and obviously a ReRegister method when it Disposes. PaymentComplete calls a TransactionComplete method on PaymentTransactionService. It checks it's list and executes the registered action if there is one - and logs an error if there isn't. You can use each PaymentComplete call to also kick off a managment routine that checks timeouts and removed expired registrations.

  • Related