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 serviceHostedCheckoutService
to raise an event toIndex.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.