DEV Community

Francisco Simões
Francisco Simões

Posted on

How to prevent Foreign Key crashes with Stripe Webhooks in .NET 10.

When integrating Stripe Subscriptions in a .NET application using Entity Framework Core, one of the most common issues developers face is dealing with the checkout.session.completed webhook.

If you have a relational database, you probably have a SubscriptionPlans table and a UserSubscriptions table.

A frequent mistake is forgetting to map the Stripe Price ID back to your internal Database Plan ID when the webhook fires. If you just try to insert the subscription, EF Core will default your Foreign Key to 0, resulting in a nasty Foreign Key constraint violation (Error 500).

Here is the clean way to handle the webhook and map the Stripe Price ID to your internal database ID:

private async Task HandleCheckoutSessionCompleted(Session session)
{
    var userId = session.ClientReferenceId; // We passed this during checkout

    var subscriptionService = new SubscriptionService();
    var stripeSubscription = await subscriptionService.GetAsync(session.SubscriptionId);

    // 1. Extract the Stripe Price ID (price_...) from the invoice items
    var stripePriceId = stripeSubscription.Items.Data.FirstOrDefault()?.Price.Id;

    // 2. Look up the internal Plan ID in your database using the Stripe Price ID
    var plan = await _context.SubscriptionPlans.FirstOrDefaultAsync(p => p.StripePriceId == stripePriceId);

    // 3. Fallback to ID 1 to prevent Foreign Key constraint errors if sync is missing
    int planId = plan != null ? plan.Id : 1; 

    // Now you can safely insert or update your user's subscription record
    var userSubscription = await _context.UserSubscriptions
        .FirstOrDefaultAsync(u => u.UserId == userId);

    if (userSubscription == null)
    {
        userSubscription = new UserSubscription
        {
            UserId = userId,
            StripeSubscriptionId = stripeSubscription.Id,
            StripeCustomerId = stripeSubscription.CustomerId,
            Status = "Active",
            SubscriptionPlanId = planId // Safe to insert!
        };
        _context.UserSubscriptions.Add(userSubscription);
    }

    await _context.SaveChangesAsync();
}
Enter fullscreen mode Exit fullscreen mode

Why this works:
Failsafe mapping: By querying your database for the StripePriceId, you ensure your relational data stays intact.

Fallback: If you forgot to add the plan to your local DB but created it on Stripe, it gracefully falls back to a default plan instead of crashing the API.

Setting up Stripe Webhooks, Identity, and Clean Architecture from scratch usually takes weeks of trial and error.

If you want to skip this headache, I actually packaged my entire setup into a Premium .NET SaaS Boilerplate. It has Authentication, Stripe Billing, the Customer Portal, and EF Core completely wired up and ready for production.

You can grab the code and start building your product today here:
👉 https://s1moes.gumroad.com/l/pdjrrt

Happy coding!

Top comments (0)