Skip to content

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(),
        };
    }
}

Extracting the necessary information from the request

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.

1
2
3
4
5
6
7
...
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.