Setting up the server callback handling
This article provides a quick guide to set up a controller accepting server callbacks
from the various payment providers in a single generic way.
Code sample
The code necessary to support callbacks consists of two parts, (1) extracting the HTTP
request information into a readable format for the extension and (2) calling the
correct callback handler. We will start with the latter, as it gives a better
overview of the whole method.
Calling the correct callback handler
The callback handlers implement a CanHandle/Handle pattern, which means you need to
dependency inject a list of them,
i.e. IEnumerable<IPaymentCallbackMessageHandler>
(_messageHandlers
below is of
that type).
| public record SampleContext(int SegmentationId) : PaymentContext;
public class PaymentGatewayController : Controller
{
...
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpPatch]
public async Task<IActionResult> HandleMessage(
string paymentIdentifier, string path)
{
// extract information from http context
// which is expanded upon later
...
var paymentMessage = new PaymentCallbackMessage(
httpMethod,
PaymentIdentifier.Create(paymentIdentifier),
path.ToLowerInvariant(),
query,
headers,
formData,
postBody,
remoteIpAddress,
isLocal);
// Simple example here, normally you could be able
// to get more information from the http method, http context etc.
var sampleContext = new SampleContext(1);
try
{
IPaymentCallbackMessageHandler<SampleContext>? handler = null;
foreach (var potentialHandler in _messageHandlers)
{
if (await potentialHandler.CanHandleAsync(paymentMessage))
{
handler = potentialHandler;
break;
}
}
if (handler is null)
return BadRequest();
var response = await handler
.HandleAsync(sampleContext, paymentMessage);
return HandleResponse(response);
}
catch (PaymentException ex)
{
return BadRequest(ex.Message);
}
}
private IActionResult HandleResponse(PaymentResponse response)
{
return response switch
{
PaymentResponseRedirect redirectResponse
=> Redirect(redirectResponse.AbsoluteUrl),
PaymentResponseStatusCode statusCodeResponse
=> StatusCode(statusCodeResponse.StatusCode),
PaymentResponseContent contentResponse
=> Content(contentResponse.Content),
_ => Ok(),
};
}
}
|
You can use the below code snippet as inspiration for how to extract the required
properties for the PaymentCallbackMessage
.
| var httpMethod = HttpContext.Request.Method.ToLowerInvariant();
var query = HttpContext.Request.Query
.ToDictionary(
e => e.Key,
e => e.Value
.Where(x => x is not null).ToArray() as string[],
StringComparer.Ordinal);
var headers = HttpContext.Request.Headers
.ToDictionary(
e => e.Key,
e => e.Value
.Where(x => x is not null).ToArray() as string[],
StringComparer.Ordinal);
var postBody = await HttpContext.Request.GetPostBodyAsync();
var formData = HttpContext.Request.GetFormData();
var remoteIpAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
var isLocal = HttpContext.Request.IsLocal()
|
The extension methods used to gather the postBody
, formData
, and isLocal
could
be implemented as seen below.
| public static class HttpRequestExtensions
{
public static bool IsLocal(this HttpRequest req)
{
if (req.Headers.ContainsKey("X-Forwarded-For"))
{
return false;
}
var connection = req.HttpContext.Connection;
if (connection.RemoteIpAddress != null)
{
return connection.LocalIpAddress != null
? connection.RemoteIpAddress
.Equals(connection.LocalIpAddress)
: IPAddress.IsLoopback(connection.RemoteIpAddress);
}
// for in memory TestServer or
// when dealing with default connection info
if (connection.RemoteIpAddress == null
&& connection.LocalIpAddress == null)
{
return true;
}
return false;
}
public static async Task<string> GetPostBodyAsync(
this HttpRequest request)
{
string bodyStr;
// Allows using the stream multiple times
request.EnableBuffering();
// important: keep stream opened
using (var reader = new StreamReader(
request.Body,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
1024,
leaveOpen: true))
{
bodyStr = await reader.ReadToEndAsync();
}
request.Body.Position = 0;
return bodyStr;
}
public static Dictionary<string, string[]>? GetFormData(
this HttpRequest request)
{
if (!request.HasFormContentType)
{
return null;
}
return request.Form
.ToDictionary(
e => e.Key,
e => e.Value
.Where(x => x is not null).ToArray() as string[],
StringComparer.Ordinal);
}
}
|
Registering the route
Lastly, before the endpoint is set up properly, we add it as a route in our
controller setup.
| ...
var app = builder.Build();
app.MapControllerRoute(
"payment-gateway",
$"paymentgateway/{{paymentIdentifier}}/{{*path}}",
new { controller = "paymentgateway", action = "HandleMessage" });
...
|
Next step
If you have followed all the steps so far in the "Getting Started" guide, you are
now done with the initial setup. The next step is to look at the documentation for
the payment providers you are interested in using under the integrations section.