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