Back to Blog
Apr 09, 2023

Integrate Stripe Hosted Checkout Into .NET Web Application

Written by Zack Schwartz


Intro

As of the writing of this blog post, Stripe is the best payment provider in the world when it comes to providing a Developer Experience. It is often cited as the gold standard of API and documentation. Having said that, integrating payments into your .NET application is still a technical endeavor, and we will cover the elements you need in this article and Youtube video.

We are building our application on top of the Raytha platform so that we can focus just on the functionality we want and everything else is handled for us.

Event Registration Platform w/ Stripe Payments

To demonstrate an integration with Stripe Hosted Checkout, we built a conference / event registration portal where visitors can purchase tickets for themselves or others. Take a look at the animation below to see how this works.



Filling out the information and clicking Purchase Tickets will take us to the Stripe Hosted Checkout page with the proper total amount based on the number and types of tickets that were selected.



Required .NET Controller Endpoints

We created a TicketsController to Raytha with a "/tickets/buy" GET and POST endpoints and "/tickets/success" to our Raytha platform.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Raytha.Application.Tickets;
using Raytha.Application.Tickets.Commands;
using Raytha.Domain.Entities;
using Raytha.Web.Areas.Public.Views.Tickets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

namespace Raytha.Web.Areas.Public.Controllers;

[Area("Public")]
[Authorize]
public class TicketsController : BaseController
{
    [Route("tickets/buy", Name = "ticketsbuy")]
    public async Task<IActionResult> Buy()
    {
        if (Request.Cookies.TryGetValue("myTickets", out string modelJson))
        {
            var model = JsonSerializer.Deserialize<BuyTickets_ViewModel>(modelJson);
            return View(model);
        }
        else
        {
            var model = new BuyTickets_ViewModel
            {
                Tickets = new List<BuyTicketItem_ViewModel>() { new BuyTicketItem_ViewModel() }
            };
            return View(model);
        }
    }

    [Route("tickets/buy", Name = "ticketsbuy")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Buy(BuyTickets_ViewModel model)
    {
        var tickets = model.Tickets.Select(p => new PurchaseTicketDto
        {
            FirstName = p.FirstName,
            LastName = p.LastName,
            EmailAddress = p.EmailAddress?.Trim(),
            Organization = p.Organization,
            TicketType = p.TicketType
        });
        var response = await Mediator.Send(new BeginBuyTicket.Command
        {
            PurchaserUserId = CurrentUser.UserId.Value,
            Tickets = tickets
        });

        if (response.Success)
        {
            string modelJson = JsonSerializer.Serialize(model);
            Response.Cookies.Append("myTickets", modelJson, new CookieOptions
            {
                Expires = DateTimeOffset.UtcNow.AddDays(1)
            });
            string url = response.Result;
            this.HttpContext.Response.StatusCode = 303;
            return View("RedirectToCheckout", url);
        }
        SetErrorMessage("There were errors with your submission. Please see below.", response.GetErrors());
        return View(model);
    }

    [Route("tickets/success", Name = "ticketssuccess")]
    public async Task<IActionResult> Success(string session_id)
    {
        var result = await Mediator.Send(new CompleteBuyTicket.Command
        { 
            SessionId = session_id 
        });

        Response.Cookies.Delete("myTickets");

        return View();
    }
}


Application Service Layer

Raytha follows the CleanArchitecture pattern which is based on CQRS. We have an Application layer where we added Commands for ticket purchasing.



Our Commands.BeginBuyTicket's handler is as follows:

    public class Handler : IRequestHandler<Command, CommandResponseDto<string>>
    {
        private readonly IRaythaDbContext _db;
        private readonly IPaymentProvider _paymentProvider;
        public Handler(IRaythaDbContext db, IPaymentProvider paymentProvider)
        {
            _db = db;
            _paymentProvider = paymentProvider;
        }

        public async Task<CommandResponseDto<string>> Handle(Command request, CancellationToken cancellationToken)
        {
            var tickets = request.Tickets.Select(p => new Ticket
            {
                FirstName = p.FirstName,
                LastName = p.LastName,
                EmailAddress = p.EmailAddress,
                Organization = p.Organization,
                TicketType = p.TicketType
            });

            var purchaser = _db.Users.FirstOrDefault(p => p.Id == request.PurchaserUserId.Guid);

            var sessionUrl = _paymentProvider.BeginPurchaseTickets(purchaser, tickets);
            return new CommandResponseDto<string>(sessionUrl);
        }
    }

This code references IPaymentProvider which is an interface that a StripePaymentProvider class implements. This is because you may want to swap out Stripe for something else, but you do not need to rewrite your interface.

Infrastructure Layer

And finally, we have the implementation for the StripePaymentProvider which is defined as follows:

using Amazon.S3.Model;
using Microsoft.Extensions.Configuration;
using Raytha.Application.Common.Interfaces;
using Raytha.Domain.Entities;
using Stripe;
using Stripe.Checkout;
using System.Text.Json;

namespace Raytha.Infrastructure.Services;

public class StripePaymentProvider : IPaymentProvider
{
    private readonly IConfiguration _configuration;
    private readonly IRelativeUrlBuilder _urlBuilder;
    public StripePaymentProvider(IRelativeUrlBuilder urlBuilder, IConfiguration configuration)
    {
        _configuration = configuration;
        _urlBuilder = urlBuilder;
    }

    public string BeginPurchaseTickets(User purchaser, IEnumerable<Ticket> tickets)
    {
        var options = new SessionCreateOptions
        {
            CustomerEmail = purchaser.EmailAddress,
            LineItems = GetSessionLineItems(tickets),
            Mode = "payment",
            SuccessUrl = $"{_urlBuilder.UserTicketPurchaseSuccess()}?session_id={{CHECKOUT_SESSION_ID}}",
            CancelUrl = _urlBuilder.UserTicketPurchaseCancel(),
            Metadata = tickets.ToDictionary(k => k.EmailAddress, v => JsonSerializer.Serialize(v))
        };

        var service = new SessionService();
        Session session = service.Create(options, new Stripe.RequestOptions
        {
            ApiKey = _configuration["STRIPE_SECRET_KEY"]
        });

        return session.Url;
    }

    public Tuple<string, object> GetTicketsFromSessionId(string sessionId)
    {
        var options = new SessionGetOptions();

        var service = new SessionService();
        // Retrieve the session. If you require line items in the response, you may include them by expanding line_items.
        Session session = service.Get(sessionId, options, new Stripe.RequestOptions
        {
            ApiKey = _configuration["STRIPE_SECRET_KEY"]
        });

        return new Tuple<string, object>(session.PaymentIntentId, session.Metadata);
    }

    private List<SessionLineItemOptions> GetSessionLineItems(IEnumerable<Ticket> tickets)
    {
        var lineItems = new List<SessionLineItemOptions>();
        var groupedByPrice = tickets.GroupBy(p => p.TicketType);
        foreach (var group in groupedByPrice)
        {
            lineItems.Add(new SessionLineItemOptions
            {
                Price = group.Key,
                Quantity = group.Count()
            });
        }
        return lineItems;
    }
}

Total level of effort: ~6 hours of developer labor.

I would say another 48-60 hours of developer labor are required to get it to a professional level. But if you are interested in seeing this in action, or would like to talk to a developer, reach out to [email protected]

Cheers.

picture of the author
Zack Schwartz @apexdodge

ENTREPRENEUR & SOFTWARE ENGINEER, AUSTIN, TX
I enjoy tackling a wide array of business challenges ranging from front line product support and operations to sales and marketing efforts. My core expertise is in software development building enterprise level web applications.


Subscribe to the Raytha newsletter