<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RGK Publication]]></title><description><![CDATA[RGK Publication]]></description><link>https://rgk.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 28 Oct 2026 12:06:48 GMT</lastBuildDate><atom:link href="https://rgk.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Send emails in ASP.NET Core with SendGrid and a background queue]]></title><description><![CDATA[Send order confirmation emails in ASP.NET Core with SendGrid, Channels and a BackgroundService, so a slow or failed email never breaks checkout.]]></description><link>https://rgk.hashnode.dev/send-emails-in-asp-net-core-with-sendgrid-and-a-background-queue</link><guid isPermaLink="true">https://rgk.hashnode.dev/send-emails-in-asp-net-core-with-sendgrid-and-a-background-queue</guid><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[email]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:36:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/e6431d68-05c4-4e4a-b010-f3403a1568da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Customers expect an email the moment they order, and another each time the order status changes. The first version most of us write sends the email right inside the checkout request. It works until the email provider is slow, or down, and suddenly checkout is slow, or fails, for a reason that has nothing to do with the order.</p>
<p>Here's how to send order emails with SendGrid in ASP.NET Core, with simplified code. Order and status-update emails are part of <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce platform</a>, so this is a problem I've spent time on.</p>
<h2>Rule one: an email must never fail an order</h2>
<p>By the time you send the confirmation, the payment is captured and the order is saved. If the email throws, the customer must still see "Thank you for your order", not an error page. So the checkout only <em>queues</em> the email; something else sends it.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d13.png" alt="Checkout saves the order and queues the email; a background service sends it through SendGrid" /></p>
<h2>A small in-memory queue</h2>
<p><code>System.Threading.Channels</code> makes a simple, fast queue:</p>
<pre><code class="language-csharp">public record EmailMessage(string To, string Subject, string Html);

public class EmailQueue
{
    private readonly Channel&lt;EmailMessage&gt; channel =
        Channel.CreateBounded&lt;EmailMessage&gt;(new BoundedChannelOptions(1000)
        {
            FullMode = BoundedChannelFullMode.Wait
        });

    public ValueTask EnqueueAsync(EmailMessage message) =&gt; channel.Writer.WriteAsync(message);
    public IAsyncEnumerable&lt;EmailMessage&gt; ReadAllAsync(CancellationToken ct) =&gt; channel.Reader.ReadAllAsync(ct);
}
</code></pre>
<p>Register it as a singleton, and checkout just does:</p>
<pre><code class="language-csharp">await emailQueue.EnqueueAsync(new EmailMessage(
    order.Email,
    $"Order #{order.Id} confirmed",
    await templates.RenderAsync("OrderConfirmation", order)));
</code></pre>
<h2>A background service that sends</h2>
<pre><code class="language-csharp">public class EmailSenderService(EmailQueue queue, IServiceScopeFactory scopes, ILogger&lt;EmailSenderService&gt; log)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var message in queue.ReadAllAsync(stoppingToken))
        {
            try
            {
                using var scope = scopes.CreateScope();
                var sender = scope.ServiceProvider.GetRequiredService&lt;IEmailSender&gt;();
                await sender.SendAsync(message, stoppingToken);
            }
            catch (Exception ex)
            {
                log.LogError(ex, "Couldn't send email to {To}: {Subject}", message.To, message.Subject);
            }
        }
    }
}
</code></pre>
<p>The <code>try/catch</code> is inside the loop on purpose. One failed email is logged, and the service keeps sending the rest.</p>
<p>The scope matters too. A <code>BackgroundService</code> is a singleton, so anything scoped, like a <code>DbContext</code> or a settings service, has to come from a fresh scope.</p>
<h2>Sending through SendGrid</h2>
<p>The official <code>SendGrid</code> NuGet package keeps it short:</p>
<pre><code class="language-csharp">public class SendGridEmailSender(ISettingsService settings) : IEmailSender
{
    public async Task SendAsync(EmailMessage m, CancellationToken ct)
    {
        var client = new SendGridClient(await settings.GetAsync("SendGridApiKey"));
        var from = new EmailAddress(await settings.GetAsync("FromEmail"), await settings.GetAsync("StoreName"));
        var msg = MailHelper.CreateSingleEmail(from, new EmailAddress(m.To), m.Subject, null, m.Html);

        var response = await client.SendEmailAsync(msg, ct);
        if (!response.IsSuccessStatusCode)
            throw new InvalidOperationException($"SendGrid returned {(int)response.StatusCode}: {await response.Body.ReadAsStringAsync(ct)}");
    }
}
</code></pre>
<p><code>SendEmailAsync</code> doesn't throw on a rejected email; it returns the status. Check <code>IsSuccessStatusCode</code>, or failures will pass silently. The most common one is a 403 because the sender address isn't verified.</p>
<p>I read the API key from database-backed settings, so the store owner can paste it into the admin dashboard instead of editing config files.</p>
<h2>Status-update emails</h2>
<p>When an admin moves an order from "Placed" to "Shipped", the same queue sends the update. Keep the send out of the status change itself: save the new status first, then enqueue the email.</p>
<blockquote>
<p><strong>See it end to end:</strong> with the <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core 10 e-commerce source code</a>, customers get an email when they order and each time an admin updates the status.</p>
</blockquote>
<h2>The trade-off of an in-memory queue</h2>
<p>If the app restarts with emails still in the channel, they're lost. For a small store, the risk is low and the simplicity is worth it. If every email must be delivered, use the <strong>outbox pattern</strong>: write the email to a database table in the same transaction as the order, and have the background service send unsent rows and mark them sent. It survives restarts, and you get a record of every email for free.</p>
<h2>Deliverability is half the job</h2>
<p>The code is the easy part. To keep order emails out of spam, verify your domain in SendGrid (SPF and DKIM), send from an address on that domain, and include a plain, readable layout. Test with a Gmail and an Outlook address before launch.</p>
<h2>FAQ</h2>
<h3>How do I send email from ASP.NET Core?</h3>
<p>Use a provider's library, such as SendGrid's, or an SMTP client, wrap it behind an <code>IEmailSender</code> interface, and send outside the request when you can. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> uses SendGrid, with the API key set from the admin dashboard.</p>
<h3>Why do my SendGrid emails go to spam?</h3>
<p>Usually because the sending domain isn't authenticated. Set up SPF and DKIM through SendGrid's domain authentication, send from an address on that domain, and avoid image-only emails.</p>
<h3>Should I use an in-memory queue, Hangfire or an outbox?</h3>
<p>An in-memory <code>Channel</code> is simplest but loses queued emails on restart. Hangfire adds persistence, retries and a dashboard. An outbox table guarantees the email is recorded with the order in one transaction.</p>
<h2>Get the source code</h2>
<p>Order confirmation and status-update emails through SendGrid are part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code</a>.</p>
<p>Do you use an in-memory queue, an outbox table, or a job library like Hangfire for emails? Tell me in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core production checklist: going live with an e-commerce site]]></title><description><![CDATA[An ASP.NET Core deployment checklist for e-commerce: demo accounts, JWT keys, connection strings, SendGrid, PayPal Live, HTTPS, Data Protection.]]></description><link>https://rgk.hashnode.dev/asp-net-core-production-checklist-going-live-with-an-e-commerce-site</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-production-checklist-going-live-with-an-e-commerce-site</guid><category><![CDATA[dotnet]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Databases]]></category><category><![CDATA[C#]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:34:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/3c6a6245-7503-46ce-8a0a-92c8d9d07d6b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A project that runs perfectly on <code>localhost</code> is set up for development: demo data, a known admin password, placeholder keys, sandbox payments. All of that is helpful locally and dangerous in production.</p>
<p>This is the checklist I ship with <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core 10 store</a>. Most of it applies to any ASP.NET Core app going live.</p>
<h2>1. Change or remove the demo admin</h2>
<p>Seeded demo accounts have published passwords. They're in the README, the docs, sometimes the live demo page. Before launch:</p>
<ul>
<li>Create your real admin account with a strong password.</li>
<li>Delete the demo admin, or at least change its password and email.</li>
<li>Turn off demo seeding in production, so a fresh database doesn't recreate it.</li>
</ul>
<h2>2. Replace the JWT signing key</h2>
<p>If your API issues JWTs, the signing key in development config is a placeholder anyone can read. With it, anyone can mint a valid token for any user, including an admin.</p>
<p>Generate a long random key and keep it out of source control:</p>
<pre><code class="language-bash"># a 64-byte random key, base64-encoded
openssl rand -base64 64
</code></pre>
<p>Pass it in through an environment variable (<code>Jwt__Key</code>), your host's secret settings, or a key vault. Never commit it.</p>
<h2>3. Point at the production database</h2>
<p>Set the provider and connection string through environment variables rather than editing <code>appsettings.json</code>:</p>
<pre><code>DatabaseProvider=SqlServer
ConnectionStrings__SqlServer=Server=...;Database=...;User Id=...;Password=...;Encrypt=True
</code></pre>
<p>Use a database user with only the permissions the app needs. And decide how migrations run in production: at startup is fine for a single instance, but a separate deployment step is safer once you scale out.</p>
<h2>4. Set up email</h2>
<p>Order confirmations and status updates need a real email provider. For SendGrid, that means:</p>
<ul>
<li>Add your API key in the admin settings (or in configuration).</li>
<li><strong>Verify your sending domain</strong> with SPF and DKIM records. Without it, order emails land in spam.</li>
<li>Set the admin notification address.</li>
<li>Place one test order and check the email arrives, including on a mobile mail app.</li>
</ul>
<h2>5. Switch payments from Sandbox to Live</h2>
<ul>
<li>Create <strong>live</strong> PayPal app credentials. Sandbox ones won't work in production, and live ones should never be used for testing.</li>
<li>Flip the Sandbox / Live switch.</li>
<li>Register the webhook URL for the live app and confirm verification passes.</li>
<li>Make one small real purchase and refund it from the dashboard. It's the only way to know the whole loop works.</li>
</ul>
<blockquote>
<p><strong>Want payments ready to switch on?</strong> The <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core e-commerce source code</a> has a PayPal Sandbox / Live switch and one-click refunds in the dashboard.</p>
</blockquote>
<h2>6. HTTPS and security headers</h2>
<pre><code class="language-csharp">if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}
app.UseHttpsRedirection();
</code></pre>
<p>Make sure <code>ASPNETCORE_ENVIRONMENT</code> is <code>Production</code>. In <code>Development</code>, the detailed error page shows stack traces and configuration details to anyone who triggers an error.</p>
<h2>7. Persist Data Protection keys</h2>
<p>ASP.NET Core encrypts login cookies and antiforgery tokens with Data Protection keys. In containers or on hosts that reset the file system, the keys vanish on restart and everyone is logged out, or worse, forms start failing. Store them somewhere durable:</p>
<pre><code class="language-csharp">builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo("/keys"))   // a mounted volume
    .SetApplicationName("my-store");
</code></pre>
<h2>8. Logging and backups</h2>
<ul>
<li>Send logs somewhere you can search: Application Insights, Seq, or your host's log stream. You'll want them the first time a customer says "my order disappeared".</li>
<li>Turn on automated database backups, and <strong>test a restore once</strong>. A backup you've never restored is a hope, not a backup.</li>
</ul>
<h2>9. Replace the demo content</h2>
<p>Demo products, sample prices, placeholder contact details, the default store name and logo, and the terms and privacy pages. It's easy to miss one, so click through every footer link before launch.</p>
<h2>Print it, tick it</h2>
<p>None of these steps takes long. Missing one can cost far more than the time it would have taken.</p>
<h2>FAQ</h2>
<h3>How do I deploy an ASP.NET Core app to production?</h3>
<p>Publish a Release build, set <code>ASPNETCORE_ENVIRONMENT</code> to <code>Production</code>, pass secrets through environment variables, enable HTTPS, apply migrations and set up logging. Azure App Service, a Linux server behind Nginx, and Docker all work. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> comes with documentation that walks through these steps.</p>
<h3>Why are users logged out after every deployment?</h3>
<p>The Data Protection keys aren't persisted, so each new instance creates new keys and old cookies can't be read. Store the keys on a mounted volume, in blob storage or in a database.</p>
<h3>Is it safe to run with ASPNETCORE_ENVIRONMENT=Development?</h3>
<p>No. The developer exception page shows stack traces and configuration details to anyone who triggers an error.</p>
<h2>Get the source code</h2>
<p>A go-live checklist with step-by-step instructions is part of the documentation for <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code and docs</a>.</p>
<p>What's on your go-live list that isn't on mine? Add it in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core MVC project structure: a practical layered architecture]]></title><description><![CDATA[A practical layered architecture for .NET Core MVC Web, Services, Data, Entities and Shared projects, with EF Core and no repository-pattern overhead.]]></description><link>https://rgk.hashnode.dev/asp-net-core-mvc-project-structure-a-practical-layered-architecture</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-mvc-project-structure-a-practical-layered-architecture</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:31:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/b03effb2-291a-4811-a984-83b1f576cc13.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Architecture debates online tend to swing between two extremes: everything in one project, or Clean Architecture with a dozen projects and an interface for every class. For a real e-commerce app I wanted something in between that a new developer could understand in ten minutes.</p>
<p>My solution, <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a>, has five projects. Here's what each does, which way the references point, and the trade-offs.</p>
<h2>The five projects</h2>
<pre><code>eCommerce.Web        MVC controllers, views, admin dashboard, REST API
eCommerce.Services   business logic: pricing, orders, payments, email
eCommerce.Data       EF Core DbContext, migrations, configurations
eCommerce.Entities   domain classes: Product, Order, Category, PromoCode
eCommerce.Shared     helpers, constants, localization keys
</code></pre>
<p>References only point "down":</p>
<pre><code>Web  →  Services  →  Data  →  Entities
  ↘        ↓          ↓
           Shared  ←──┘
</code></pre>
<p><code>Entities</code> and <code>Shared</code> depend on nothing in the solution. <code>Web</code> never touches the <code>DbContext</code> directly; it goes through <code>Services</code>.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d11.png" alt="Five projects: Web references Services, Services references Data, Data references Entities, and all can use Shared" /></p>
<h2>Why controllers don't use the DbContext</h2>
<p>This is the rule that pays off most. When the website's checkout and the mobile API's checkout both call <code>IOrderService.PlaceOrderAsync</code>, the rules for prices, stock and promo codes exist exactly once:</p>
<pre><code class="language-csharp">public class CheckoutController(IOrderService orders) : Controller
{
    [HttpPost]
    public async Task&lt;IActionResult&gt; PlaceOrder(CheckoutForm form)
    {
        var result = await orders.PlaceOrderAsync(User.GetUserId(), form.ToRequest());
        return result.Succeeded
            ? RedirectToAction("Confirmation", new { id = result.OrderId })
            : View("Index", form.WithError(result.Error));
    }
}
</code></pre>
<p>The controller translates HTTP into a service call and back. Nothing else.</p>
<h2>Why I didn't add a repository layer</h2>
<p>EF Core's <code>DbContext</code> is already a unit of work, and each <code>DbSet</code> is already a repository. Wrapping them in <code>IProductRepository</code> with <code>GetById</code>, <code>Add</code> and <code>Update</code> mostly adds code, and it tends to hide the features that make EF Core good: projections, <code>Include</code>, <code>ExecuteUpdateAsync</code>.</p>
<p>Services use the <code>DbContext</code> directly. When a query is reused in several places, it becomes an extension method on <code>IQueryable&lt;Product&gt;</code>, for example <code>.Published()</code> or <code>.InCategory(id)</code>, not a repository class.</p>
<h2>Entities stay plain</h2>
<p>Entities are plain classes with no EF attributes. Mapping lives in <code>Data</code>, in <code>IEntityTypeConfiguration&lt;T&gt;</code> classes:</p>
<pre><code class="language-csharp">public class ProductConfiguration : IEntityTypeConfiguration&lt;Product&gt;
{
    public void Configure(EntityTypeBuilder&lt;Product&gt; b)
    {
        b.Property(p =&gt; p.Price).HasPrecision(18, 2);
        b.HasIndex(p =&gt; p.Sku).IsUnique();
    }
}
</code></pre>
<p>That keeps <code>Entities</code> free of database concerns, which matters when one model has to work on SQL Server, PostgreSQL, MySQL and SQLite.</p>
<h2>View models live in Web</h2>
<p>Views never get entities directly. A <code>ProductDetailsViewModel</code> contains exactly what the page shows. That avoids accidentally rendering fields you didn't mean to, and stops model binding from writing to properties like <code>Price</code> or <code>IsAdmin</code> that a form should never set (the over-posting problem).</p>
<blockquote>
<p><strong>Browse the structure:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore source code</a> is 100% C# and Razor, organised exactly like this.</p>
</blockquote>
<h2>What I deliberately left out</h2>
<ul>
<li><strong>MediatR and CQRS.</strong> Useful in big systems, but for this app it would double the number of classes without changing behaviour.</li>
<li><strong>A separate Application and Domain layer.</strong> <code>Services</code> plays both roles. If it grows too big, it can be split by feature later.</li>
<li><strong>Interfaces for everything.</strong> Services that need to be swapped or mocked get one, like <code>IPaymentGateway</code> or <code>IEmailSender</code>. Plain helpers don't.</li>
</ul>
<h2>Would I structure it differently next time?</h2>
<p>Maybe by feature inside each project (<code>Orders/</code>, <code>Catalog/</code>, <code>Payments/</code>) rather than by type (<code>Controllers/</code>, <code>Services/</code>). The project boundaries, though, have held up well through a full .NET 10 upgrade.</p>
<h2>FAQ</h2>
<h3>Do I need the repository pattern with EF Core?</h3>
<p>Usually not. <code>DbContext</code> is already a unit of work and each <code>DbSet</code> is a repository. Add your own abstraction only where you really need to swap the data source or hide complex queries.</p>
<h3>What's the difference between Clean Architecture and a layered architecture?</h3>
<p>Clean Architecture inverts the dependencies, so the core has no reference to infrastructure. A layered architecture lets references point down to the data layer. It's simpler, and fine for most data-driven web apps.</p>
<h3>How should I organise an ASP.NET Core solution?</h3>
<p>Split by responsibility into a few projects, and organise by feature inside them. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> uses five projects: Web, Services, Data, Entities and Shared.</p>
<h2>Get the source code</h2>
<p>That clean five-project solution, 100% source code, is <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Explore the ASP.NET Core 10 e-commerce platform</a>.</p>
<p>How do you structure your ASP.NET Core apps? Repository layer or not? Let me know in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core settings in the database: change config without a redeploy]]></title><description><![CDATA[A practical layered architecture for .NET Core MVC: Web, Services, Data, Entities and Shared projects, with EF Core and no repository-pattern overhead]]></description><link>https://rgk.hashnode.dev/asp-net-core-settings-in-the-database-change-config-without-a-redeploy</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-settings-in-the-database-change-config-without-a-redeploy</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Docker]]></category><category><![CDATA[asp.net core]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:28:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/19cfb50f-b251-46cf-8d5c-748b60d57307.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>appsettings.json</code> is great for things developers change: connection strings, logging levels. It's a poor fit for things a store owner changes: the contact email, whether PayPal is enabled, the shipping fee, the SendGrid key. Asking the owner to edit JSON and redeploy for each of those isn't realistic.</p>
<p>So <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core store</a>, keeps those settings in a database table, editable from <strong>Dashboard → Configurations</strong>. Here's the design, with simplified code.</p>
<h2>A simple key/value table</h2>
<pre><code class="language-csharp">public class Configuration
{
    public int Id { get; set; }
    public string Key { get; set; } = "";
    public string? Value { get; set; }
    public string? Description { get; set; }   // shown in the admin UI
    public bool IsSecret { get; set; }
}
</code></pre>
<p>Each row carries a description, so the admin page can explain what "EnablePayPal" or "PayPalClientSecret" does right next to the field. A unique index on <code>Key</code> prevents duplicates.</p>
<h2>A cached settings service</h2>
<p>Reading the database on every request would be wasteful, so a small service loads all settings once and caches them:</p>
<pre><code class="language-csharp">public class SettingsService(AppDbContext db, IMemoryCache cache) : ISettingsService
{
    const string CacheKey = "site-settings";

    public async Task&lt;IReadOnlyDictionary&lt;string, string?&gt;&gt; GetAllAsync() =&gt;
        (await cache.GetOrCreateAsync(CacheKey, async entry =&gt;
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
            return await db.Configurations.AsNoTracking()
                .ToDictionaryAsync(c =&gt; c.Key, c =&gt; c.Value);
        }))!;

    public async Task&lt;string?&gt; GetAsync(string key) =&gt;
        (await GetAllAsync()).GetValueOrDefault(key);

    public async Task&lt;bool&gt; GetBoolAsync(string key) =&gt;
        bool.TryParse(await GetAsync(key), out var b) &amp;&amp; b;

    public void Invalidate() =&gt; cache.Remove(CacheKey);
}
</code></pre>
<p>When the admin saves a setting, call <code>Invalidate()</code>. The next request reloads fresh values, and the change is live immediately.</p>
<p>The 30-minute expiry is a safety net. If you run more than one instance, a memory cache is per instance, so other instances pick up the change when their cache expires. For instant changes across a farm, use a distributed cache or a change-notification message.</p>
<h2>Never send secrets back to the browser</h2>
<p>Some settings are secrets: the PayPal client secret, the SendGrid API key. The admin page should let you <em>set</em> them, but never <em>show</em> them:</p>
<ul>
<li>When rendering the form, show a masked placeholder such as <code>••••••••</code> for secret rows, never the real value.</li>
<li>When saving, treat an empty secret field as "keep the current value".</li>
</ul>
<pre><code class="language-csharp">foreach (var input in form.Settings)
{
    var row = await db.Configurations.SingleAsync(c =&gt; c.Key == input.Key);
    if (row.IsSecret &amp;&amp; string.IsNullOrWhiteSpace(input.Value))
        continue;                       // left empty: keep the existing secret
    row.Value = input.Value?.Trim();
}
await db.SaveChangesAsync();
settings.Invalidate();
</code></pre>
<p>That way a secret never travels to the browser, never shows in the page source, and isn't wiped by accidentally saving the form.</p>
<p>Encrypting secret values at rest is a good next step. ASP.NET Core's Data Protection API (<code>IDataProtector.Protect</code> and <code>Unprotect</code>) does it in a few lines, as long as the key ring is persisted somewhere that survives restarts.</p>
<blockquote>
<p><strong>See the settings page:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore admin dashboard</a> lists each setting with its description, and secrets stay masked.</p>
</blockquote>
<h2>Who can change what</h2>
<p>Settings change how the store takes money, so the page is restricted to the Administrator role, not Moderators:</p>
<pre><code class="language-csharp">[Authorize(Roles = "Administrator")]
public class ConfigurationsController : Controller { ... }
</code></pre>
<h2>The same idea for translations</h2>
<p>UI translations work the same way: a table of key, culture and text, loaded into a cache and edited from the dashboard. Admins can fix a typo in the Arabic checkout page, or import a whole language pack, without touching code.</p>
<h2>What stays in appsettings.json</h2>
<p>Anything needed <em>before</em> the database is reachable stays in configuration: the connection string, the database provider, the JWT signing key and logging. Database settings are for the business; file settings are for the infrastructure.</p>
<h2>FAQ</h2>
<h3>Should settings live in appsettings.json or the database?</h3>
<p>Infrastructure settings that are needed before the database is reachable, such as connection strings, signing keys and logging, belong in configuration. Business settings that the owner changes belong in the database.</p>
<h3>How do I change ASP.NET Core settings without restarting?</h3>
<p>For files, <code>reloadOnChange</code> plus <code>IOptionsMonitor</code> picks up edits. For database settings, cache them and invalidate the cache when they're saved, as shown above.</p>
<h3>How should I store API keys in ASP.NET Core?</h3>
<p>User secrets in development; environment variables or a key vault in production. If keys live in the database, encrypt them with the Data Protection API and never send them back to the browser. In <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a>, the PayPal and SendGrid secrets are masked in the dashboard after saving.</p>
<h2>Get the source code</h2>
<p>Database-backed settings and translations, editable from the admin dashboard with secrets masked, are part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code</a>.</p>
<p>Where do you draw the line between file configuration and database settings? I'd like to hear your rule.</p>
]]></content:encoded></item><item><title><![CDATA[Prevent overselling in EF Core: concurrency-safe stock updates]]></title><description><![CDATA[Stop two customers buying the last item: fix the lost update in EF Core with a conditional ExecuteUpdateAsync or a concurrency token, on any database.]]></description><link>https://rgk.hashnode.dev/prevent-overselling-in-ef-core-concurrency-safe-stock-updates</link><guid isPermaLink="true">https://rgk.hashnode.dev/prevent-overselling-in-ef-core-concurrency-safe-stock-updates</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:25:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/23026dc8-0bba-4285-a97c-b7c2e6a704d5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Stock tracking looks like the easiest part of an online store. Every product has a quantity, and each order subtracts from it. Then a sale goes live, two people buy the last item in the same second, and you've sold something you don't have.</p>
<p>Here's why the obvious code fails, and the small change that fixes it on SQL Server, PostgreSQL, MySQL and SQLite alike. Stock tracking is a core part of <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce platform</a>, so it's a problem I've had to think through. The code is simplified.</p>
<h2>The obvious code</h2>
<pre><code class="language-csharp">var product = await db.Products.FindAsync(productId);
if (product.StockQuantity &lt; quantity)
    return Fail("Not enough stock.");

product.StockQuantity -= quantity;
await db.SaveChangesAsync();
</code></pre>
<p>Run it for two requests at once with one item in stock:</p>
<ol>
<li>Request A reads <code>StockQuantity = 1</code>. The check passes.</li>
<li>Request B reads <code>StockQuantity = 1</code>. The check passes.</li>
<li>A saves 0.</li>
<li>B saves 0.</li>
</ol>
<p>Two orders, one item, and the stock says 0 as if nothing went wrong. It's the classic lost update, and nothing in that code will warn you.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d09.png" alt="A read-then-write stock check sells one item twice; a single conditional UPDATE lets only one order through" /></p>
<h2>Fix 1: a conditional update</h2>
<p>Let the database check and subtract in one statement:</p>
<pre><code class="language-csharp">var updated = await db.Products
    .Where(p =&gt; p.Id == productId &amp;&amp; p.StockQuantity &gt;= quantity)
    .ExecuteUpdateAsync(s =&gt; s.SetProperty(p =&gt; p.StockQuantity, p =&gt; p.StockQuantity - quantity));

if (updated == 0)
    return Fail("Sorry, that item just sold out.");
</code></pre>
<p>That becomes a single <code>UPDATE ... SET StockQuantity = StockQuantity - @qty WHERE Id = @id AND StockQuantity &gt;= @qty</code>. The database locks the row while it runs, so only one of the two requests can match the <code>WHERE</code> clause. The other updates zero rows and gets a friendly message instead of an order.</p>
<p>This is my default. It's short, fast, needs no schema change, and behaves the same on every provider.</p>
<h2>Fix 2: optimistic concurrency</h2>
<p>If you'd rather keep the load-modify-save style, add a concurrency token. EF Core then includes the original value in the <code>UPDATE</code> and throws if the row changed in the meantime:</p>
<pre><code class="language-csharp">modelBuilder.Entity&lt;Product&gt;()
    .Property(p =&gt; p.StockQuantity)
    .IsConcurrencyToken();
</code></pre>
<pre><code class="language-csharp">try
{
    product.StockQuantity -= quantity;
    await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // someone else changed the stock: reload and check again, or tell the shopper
}
</code></pre>
<p>On SQL Server you could use a <code>rowversion</code> column instead, but that's SQL Server-specific. Marking the stock column itself as the token works on all four databases.</p>
<h2>Multiple items in one order</h2>
<p>A cart with three products needs all three decrements to succeed, or none. Wrap them, and saving the order, in one transaction:</p>
<pre><code class="language-csharp">await using var tx = await db.Database.BeginTransactionAsync();

foreach (var line in cart.Lines)
{
    var ok = await db.Products
        .Where(p =&gt; p.Id == line.ProductId &amp;&amp; p.StockQuantity &gt;= line.Quantity)
        .ExecuteUpdateAsync(s =&gt; s.SetProperty(p =&gt; p.StockQuantity, p =&gt; p.StockQuantity - line.Quantity));

    if (ok == 0)
    {
        await tx.RollbackAsync();
        return Fail($"{line.ProductName} doesn't have enough stock left.");
    }
}

db.Orders.Add(order);
await db.SaveChangesAsync();
await tx.CommitAsync();
</code></pre>
<p>If anything fails, the rollback puts the earlier lines' stock back.</p>
<blockquote>
<p><strong>Building a store?</strong> <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> gives you stock tracking, a low-stock filter and a server-checked checkout in ASP.NET Core 10.</p>
</blockquote>
<h2>Where payments fit in</h2>
<p>With online payments there's a second race: the stock can run out between "Pay" and the payment being captured. My order is: re-check stock, capture the payment, then save the order and decrement stock in one transaction. If that final step fails, refund automatically. I covered that flow in my article on taking PayPal payments safely.</p>
<h2>Show low stock before it's zero</h2>
<p>The other half of stock is the admin side. A "low stock" filter on the product list, for example <code>StockQuantity &lt;= 5</code>, tells the store owner what to reorder before customers see "sold out".</p>
<h2>FAQ</h2>
<h3>What is a lost update?</h3>
<p>Two transactions read the same value, both change it, and the second save overwrites the first. Neither fails, so the bug stays silent until the numbers don't add up.</p>
<h3>Should I use ExecuteUpdateAsync or a concurrency token?</h3>
<p><code>ExecuteUpdateAsync</code> (EF Core 7 and later) checks and changes stock in one statement with no retry logic. A concurrency token keeps the load-modify-save style but needs you to catch <code>DbUpdateConcurrencyException</code> and retry. Both work on every major provider.</p>
<h3>Should I reserve stock when an item is added to the cart?</h3>
<p>Usually not for a small store: reserving at checkout is simpler. Cart reservations need expiry times and clean-up, or abandoned carts lock up stock. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> shows stock on every product page and lets admins filter for low stock.</p>
<h2>Get the source code</h2>
<p>Stock tracking with a low-stock filter in the admin dashboard is part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code</a>.</p>
<p>Have you been bitten by a lost update in production? Tell me how you found it.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core promo codes: validating discounts safely with EF Core]]></title><description><![CDATA[Build coupon and promo code logic in ASP.NET Core: expiry dates, usage limits, minimum order values, decimal rounding and atomic usage counts in EF Co]]></description><link>https://rgk.hashnode.dev/asp-net-core-promo-codes-validating-discounts-safely-with-ef-core</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-promo-codes-validating-discounts-safely-with-ef-core</guid><category><![CDATA[Docker]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:24:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/b7127038-4482-4fd0-9755-d658c554be5f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A promo code looks like a small feature: a text box and a percentage. Then a code meant for 100 people gets used 3,000 times, an expired code still works because of a time-zone bug, or a 10% discount turns a $0.05 item into a negative total.</p>
<p>Here's how I model and apply promo codes in <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce store</a>, with simplified code.</p>
<h2>The model</h2>
<pre><code class="language-csharp">public class PromoCode
{
    public int Id { get; set; }
    public string Code { get; set; } = "";          // stored upper-case
    public DiscountType Type { get; set; }           // Percent or FixedAmount
    public decimal Value { get; set; }
    public decimal MinimumOrder { get; set; }
    public DateTime? StartsAtUtc { get; set; }
    public DateTime? ExpiresAtUtc { get; set; }
    public int? MaxUses { get; set; }                // null = unlimited
    public int TimesUsed { get; set; }
    public bool IsActive { get; set; } = true;
}
</code></pre>
<p>Give <code>Code</code> a unique index, and store it upper-case so <code>summer10</code> and <code>SUMMER10</code> are the same code on every database. (PostgreSQL compares strings case-sensitively by default, so this matters.)</p>
<h2>Validate everything on the server</h2>
<p>The browser only ever sends the code. The server looks it up and recalculates the cart from the database, every time the total is shown and again at checkout:</p>
<pre><code class="language-csharp">public async Task&lt;PromoResult&gt; ValidateAsync(string input, decimal subtotal)
{
    var code = input.Trim().ToUpperInvariant();
    var now = DateTime.UtcNow;

    var promo = await db.PromoCodes.AsNoTracking().SingleOrDefaultAsync(p =&gt; p.Code == code);

    if (promo is null || !promo.IsActive)                  return PromoResult.Invalid("This code isn't valid.");
    if (promo.StartsAtUtc &gt; now)                            return PromoResult.Invalid("This code isn't active yet.");
    if (promo.ExpiresAtUtc &lt;= now)                          return PromoResult.Invalid("This code has expired.");
    if (promo.MaxUses is int max &amp;&amp; promo.TimesUsed &gt;= max) return PromoResult.Invalid("This code has been fully used.");
    if (subtotal &lt; promo.MinimumOrder)                      return PromoResult.Invalid($"Spend at least {promo.MinimumOrder:0.00} to use this code.");

    return PromoResult.Valid(promo, CalculateDiscount(promo, subtotal));
}
</code></pre>
<p>Store dates in UTC and compare with <code>DateTime.UtcNow</code>. "Expires at midnight" in the admin's time zone should be converted to UTC once, when the code is saved.</p>
<h2>Round once, and never go below zero</h2>
<pre><code class="language-csharp">static decimal CalculateDiscount(PromoCode promo, decimal subtotal)
{
    var discount = promo.Type == DiscountType.Percent
        ? subtotal * promo.Value / 100m
        : promo.Value;

    discount = Math.Round(discount, 2, MidpointRounding.AwayFromZero);
    return Math.Min(discount, subtotal);   // a discount can't make the total negative
}
</code></pre>
<p>Use <code>decimal</code>, never <code>double</code>, for money. And round at one well-defined point. Rounding every line and then the total again gives totals that are off by a cent, and payment providers reject orders whose lines don't add up.</p>
<h2>Count usage atomically</h2>
<p>The classic bug: two shoppers check out at the same moment with the last use of a code. Both read <code>TimesUsed = 99</code>, both pass the check, both save 100.</p>
<p>Fix it with a conditional update, so the database does the check and the increment in one statement:</p>
<pre><code class="language-csharp">var updated = await db.PromoCodes
    .Where(p =&gt; p.Id == promoId &amp;&amp; (p.MaxUses == null || p.TimesUsed &lt; p.MaxUses))
    .ExecuteUpdateAsync(s =&gt; s.SetProperty(p =&gt; p.TimesUsed, p =&gt; p.TimesUsed + 1));

if (updated == 0)
    return CheckoutResult.Failed("This code has just been used up.");
</code></pre>
<p><code>ExecuteUpdateAsync</code> works the same on SQL Server, PostgreSQL, MySQL and SQLite. If the update touches zero rows, someone got there first.</p>
<p>Do this as part of placing the order, in the same transaction as saving it, so a failed order doesn't burn a use.</p>
<blockquote>
<p><strong>See it in action:</strong> in the <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core e-commerce platform</a>, promo codes are managed from the admin dashboard and applied at checkout.</p>
</blockquote>
<h2>Save what was applied</h2>
<p>Store the code and the discount amount on the order itself, not just a link to the promo. If the admin later edits the code from 10% to 20%, old orders and invoices must still show what the customer actually paid.</p>
<h2>Small touches that reduce support emails</h2>
<ul>
<li>Explain <em>why</em> a code failed: expired, not active yet, minimum not reached.</li>
<li>Re-validate when the cart changes. Removing an item can drop the subtotal below the minimum.</li>
<li>Let admins deactivate a code instantly instead of deleting it, so order history stays intact.</li>
</ul>
<h2>FAQ</h2>
<h3>How do I stop a coupon code being used too many times?</h3>
<p>Increment the usage count with a conditional update, <code>WHERE TimesUsed &lt; MaxUses</code>, in the same transaction that saves the order. If it updates zero rows, the code is used up.</p>
<h3>Should discounts be calculated in JavaScript or on the server?</h3>
<p>On the server. JavaScript can show a preview, but the total that's charged must be rebuilt from the database at checkout. In <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a>, prices are re-checked on the server before any payment.</p>
<h3>Should I use decimal or double for prices in C#?</h3>
<p>Always <code>decimal</code>. <code>double</code> stores binary fractions, so values like 0.1 can't be represented exactly, and totals drift by a cent. Set an explicit precision on money columns too.</p>
<h2>Get the source code</h2>
<p>Promo codes and discounts, managed from the admin dashboard and checked on the server, are part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code</a>.</p>
<p>What's the strangest promo-code abuse you've seen? Share it in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[EF Core seed data: demo data that works on the first run]]></title><description><![CDATA[Seed an ASP.NET Core app with EF Core: run migrations at startup, create Identity roles and an admin user, and load demo products only once.]]></description><link>https://rgk.hashnode.dev/ef-core-seed-data-demo-data-that-works-on-the-first-run</link><guid isPermaLink="true">https://rgk.hashnode.dev/ef-core-seed-data-demo-data-that-works-on-the-first-run</guid><category><![CDATA[Docker]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Azure]]></category><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:22:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/ff7eb519-d5fc-4da9-b8b6-42ffefcca206.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When someone downloads a project, the first five minutes decide whether they keep going. An empty store with a blank product page is a bad first impression. A store with categories, pictures and 171 products looks like the real thing.</p>
<p>So <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce project</a>, creates and seeds its database on first run. Here's how to do it without duplicate rows, race conditions or a demo password left in production. The code is simplified.</p>
<h2>Migrations first, then seed</h2>
<p>Run migrations at startup, then seed only if the data isn't there yet:</p>
<pre><code class="language-csharp">using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    var db = services.GetRequiredService&lt;AppDbContext&gt;();

    await db.Database.MigrateAsync();
    await DemoData.SeedAsync(db,
        services.GetRequiredService&lt;UserManager&lt;AppUser&gt;&gt;(),
        services.GetRequiredService&lt;RoleManager&lt;IdentityRole&gt;&gt;());
}

app.Run();
</code></pre>
<p><code>MigrateAsync</code> creates the database if it doesn't exist, so there's no separate "create database" step.</p>
<h2>Make every seed step idempotent</h2>
<p>Each step checks before it inserts. Running the app twice must never create two copies of anything:</p>
<pre><code class="language-csharp">public static async Task SeedAsync(AppDbContext db, UserManager&lt;AppUser&gt; users, RoleManager&lt;IdentityRole&gt; roles)
{
    foreach (var role in new[] { "Administrator", "Moderator", "User" })
        if (!await roles.RoleExistsAsync(role))
            await roles.CreateAsync(new IdentityRole(role));

    if (await users.FindByNameAsync("admin") is null)
    {
        var admin = new AppUser { UserName = "admin", Email = "admin@example.com", EmailConfirmed = true };
        await users.CreateAsync(admin, "Change_Me_123!");
        await users.AddToRoleAsync(admin, "Administrator");
    }

    if (!await db.Categories.AnyAsync())
    {
        db.Categories.AddRange(DemoCatalog.Categories());
        await db.SaveChangesAsync();
    }

    if (!await db.Products.AnyAsync())
    {
        db.Products.AddRange(DemoCatalog.Products(db.Categories.ToList()));
        await db.SaveChangesAsync();
    }
}
</code></pre>
<p>Roles and users go through Identity's managers, not straight into the tables. That way passwords are hashed and normalised names are set correctly.</p>
<h2>HasData or runtime seeding?</h2>
<p>EF Core's <code>HasData</code> puts seed rows into migrations. It's great for small, fixed lookup data, such as order statuses. For demo data it's a poor fit:</p>
<ul>
<li>Every change to the demo catalog produces a new migration, with every row in it.</li>
<li>It needs fixed primary keys for every row.</li>
<li>It can't hash passwords or call services.</li>
</ul>
<p>For 171 products with pictures and translations, runtime seeding is much easier to maintain.</p>
<p>EF Core 9 also added <code>UseSeeding</code> and <code>UseAsyncSeeding</code> on the <code>DbContext</code> options, which run as part of <code>EnsureCreated</code> and <code>Migrate</code>. It's a tidy place for the same idempotent code, if you'd rather keep it next to the context configuration.</p>
<h2>Keep the demo data out of C#</h2>
<p>I keep the catalog in a JSON file that ships with the app and load it in <code>DemoCatalog</code>. A non-developer can edit product names and prices, and the C# stays short. Pictures go in <code>wwwroot</code> with matching file names.</p>
<blockquote>
<p><strong>See the result:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core store demo</a> starts with categories, pictures and 171 demo products, straight from the first run.</p>
</blockquote>
<h2>Watch out for multiple instances</h2>
<p>If two instances of the app start at the same time (two containers, or a scaled-out web app), both can run the "is it empty?" check before either inserts. For production, run migrations as a separate deployment step rather than at app startup. For a demo or a single instance, startup seeding is fine.</p>
<h2>The demo admin is a demo account</h2>
<p>A seeded admin with a known password is perfect locally and dangerous in production. My go-live checklist starts with "change or remove the demo admin". Better still, read the initial admin password from configuration, or skip creating the admin unless an environment variable asks for it.</p>
<h2>FAQ</h2>
<h3>Should I use HasData or custom seeding in EF Core?</h3>
<p>Use <code>HasData</code> for small, fixed lookup data that belongs in migrations. Use runtime seeding, or <code>UseSeeding</code> / <code>UseAsyncSeeding</code> in EF Core 9 and later, for demo data, large catalogs and Identity users.</p>
<h3>How do I seed Identity users and roles in ASP.NET Core?</h3>
<p>Create a scope at startup, resolve <code>RoleManager</code> and <code>UserManager</code>, and create each role and user only if it doesn't exist yet. Going through the managers hashes passwords and sets normalised names correctly.</p>
<h3>Should I run EF Core migrations at application startup?</h3>
<p>For a single instance or a demo, yes. With several instances in production, apply them as a deployment step, for example with a migration bundle or a SQL script. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> migrates on startup, so the first run needs no setup.</p>
<h2>Get the source code</h2>
<p>Database creation and seeding on first run, for SQL Server, PostgreSQL, MySQL and SQLite, are built into <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code</a> and have a working store running in minutes.</p>
<p>Do you seed in code, from JSON or with SQL scripts? I'd like to know what works for you.</p>
]]></content:encoded></item><item><title><![CDATA[Docker Compose profiles for ASP.NET Core: one file, four databases]]></title><description><![CDATA[Run an ASP.NET Core app against SQL Server, PostgreSQL, MySQL or SQLite with Docker Compose profiles, healthchecks and depends_on service_healthy.]]></description><link>https://rgk.hashnode.dev/docker-compose-profiles-for-asp-net-core-one-file-four-databases</link><guid isPermaLink="true">https://rgk.hashnode.dev/docker-compose-profiles-for-asp-net-core-one-file-four-databases</guid><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:21:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/0a663249-55b4-4868-be88-de3818008108.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My ASP.NET Core project, <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a>, supports four databases. I wanted anyone, buyer or contributor, to try any of them with one command and no local database install.</p>
<p>The answer was Docker Compose <strong>profiles</strong>. One <code>docker-compose.yml</code>, one profile per database:</p>
<pre><code class="language-bash">docker compose --profile sqlite up --build
docker compose --profile postgres up --build
docker compose --profile mysql up --build
docker compose --profile sqlserver up --build
</code></pre>
<p>Here's how it's put together. The YAML below is simplified.</p>
<h2>Profiles in one sentence</h2>
<p>A service with <code>profiles: [postgres]</code> only starts when you pass <code>--profile postgres</code>. Services without a profile always start. That lets one file describe four different setups without them all starting at once.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d06.png" alt="Four Compose profiles, each starting its database with a healthcheck and an app service that waits for it" /></p>
<h2>The database services</h2>
<pre><code class="language-yaml">services:
  postgres:
    image: postgres:17
    profiles: [postgres]
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
      POSTGRES_DB: shop
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d shop"]
      interval: 5s
      retries: 20

  mysql:
    image: mysql:8.4
    profiles: [mysql]
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
      MYSQL_DATABASE: shop
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      retries: 20

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    profiles: [sqlserver]
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: ${DB_PASSWORD:-Dev_Passw0rd!}
    healthcheck:
      test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -Q 'SELECT 1'"]
      interval: 10s
      retries: 20
</code></pre>
<p>Two details are worth calling out. SQL Server's newer images ship <code>sqlcmd</code> under <code>mssql-tools18</code>, and it needs <code>-C</code> to trust the self-signed certificate. And <code>$$</code> escapes the dollar sign, so the variable is read inside the container, not by Compose.</p>
<h2>One app service per profile</h2>
<p>The app needs to wait for a <em>different</em> database in each profile, so I define one app service per profile. A YAML anchor keeps the shared parts in one place:</p>
<pre><code class="language-yaml">x-app: &amp;app
  build: .
  ports: ["8080:8080"]

services:
  app-postgres:
    &lt;&lt;: *app
    profiles: [postgres]
    environment:
      DatabaseProvider: PostgreSQL
      ConnectionStrings__PostgreSQL: Host=postgres;Database=shop;Username=postgres;Password=${DB_PASSWORD:-Dev_Passw0rd!}
    depends_on:
      postgres:
        condition: service_healthy

  app-sqlite:
    &lt;&lt;: *app
    profiles: [sqlite]
    environment:
      DatabaseProvider: Sqlite
      ConnectionStrings__Sqlite: Data Source=/data/shop.db
    volumes: ["sqlite-data:/data"]

volumes:
  sqlite-data:
</code></pre>
<p><code>ConnectionStrings__PostgreSQL</code> uses the double underscore that ASP.NET Core maps to <code>ConnectionStrings:PostgreSQL</code>. No config file changes are needed per environment.</p>
<blockquote>
<p><strong>Try the profiles:</strong> <a href="https://rgkjobber.gumroad.com/l/zkcwon">download the ASP.NET Core store with a Docker Compose profile for every database</a> and run <code>docker compose --profile sqlite up --build</code>.</p>
</blockquote>
<h2>Why the healthcheck matters</h2>
<p>Without <code>condition: service_healthy</code>, <code>depends_on</code> only waits for the database <em>container</em> to start, not for the database to accept connections. SQL Server in particular takes several seconds to get ready. The app would start, try to run migrations, fail and exit. With healthchecks, the app starts only when the database answers.</p>
<p>I still keep EF Core's retry logic on for the providers that support it, such as <code>EnableRetryOnFailure()</code> for SQL Server. That covers short network blips in production too, not just the startup race.</p>
<h2>SQLite needs a volume</h2>
<p>SQLite is just a file. If it lives inside the container, it disappears on every rebuild. A named volume keeps the demo data between runs. It's the profile I recommend for a first try: no database container, fastest start.</p>
<h2>Migrations and seeding on startup</h2>
<p>On first start, the app applies the right migrations for the chosen provider and seeds demo data. So <code>docker compose --profile sqlite up --build</code> ends with a working store at <code>http://localhost:8080</code>, products and all. How the seeding works is a separate article.</p>
<h2>Development defaults, not production settings</h2>
<p>The default passwords above are for local use only. For anything public, pass real secrets through an <code>.env</code> file that isn't committed, or through your host's secret store, and don't expose database ports.</p>
<h2>FAQ</h2>
<h3>What are Docker Compose profiles?</h3>
<p>Optional groups of services. A service with a <code>profiles</code> list only starts when one of its profiles is active, through <code>--profile</code> or the <code>COMPOSE_PROFILES</code> variable. Services without profiles always start.</p>
<h3>How do I make an ASP.NET Core container wait for its database?</h3>
<p>Give the database service a <code>healthcheck</code>, and use <code>depends_on</code> with <code>condition: service_healthy</code> on the app. Keep EF Core's retry-on-failure enabled as well, for short outages after startup.</p>
<h3>How do I pass a connection string to ASP.NET Core in Docker?</h3>
<p>Use an environment variable with a double underscore, such as <code>ConnectionStrings__PostgreSQL</code>. ASP.NET Core maps it to <code>ConnectionStrings:PostgreSQL</code>, which is how <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> configures each database profile.</p>
<h2>Get the source code</h2>
<p>This Compose setup ships with <strong>eCommerceMVCCore</strong>, with a profile for every supported database. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code with Docker support</a>.</p>
<p>Have you used Compose profiles, or do you keep a separate compose file per setup? Let me know in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core JWT and cookie authentication in one project]]></title><description><![CDATA[Run Identity cookie auth for an MVC site and JWT bearer auth for a REST API in one ASP.NET Core app, with 401s instead of login redirects for mobile.]]></description><link>https://rgk.hashnode.dev/asp-net-core-jwt-and-cookie-authentication-in-one-project</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-jwt-and-cookie-authentication-in-one-project</guid><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:18:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/c89490a7-b0d3-40f0-a396-deda3f1d4004.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My store has two kinds of clients. Shoppers use the website, where a normal Identity login cookie is the right choice. Mobile apps use a REST API, where a cookie is awkward and a bearer token is the norm.</p>
<p>I didn't want two backends, two user tables or two copies of the business logic. Both live in one ASP.NET Core project and share the same services. This is how <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce platform</a>, does it, with simplified code.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d05.png" alt="Website requests use the Identity cookie and redirect to login; API requests use JWT bearer and get a 401" /></p>
<h2>1. Keep Identity cookies as the default</h2>
<p><code>AddIdentity</code> (or <code>AddDefaultIdentity</code>) already registers cookie authentication and makes it the default scheme. Leave that alone. The website keeps working exactly as before.</p>
<h2>2. Add JWT bearer as a second scheme</h2>
<pre><code class="language-csharp">builder.Services.AddAuthentication()
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =&gt;
    {
        var key = builder.Configuration["Jwt:Key"]!;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    });
</code></pre>
<p>Calling <code>AddAuthentication()</code> with no arguments adds the scheme without changing the default.</p>
<h2>3. Tell the API controllers to use JWT</h2>
<p>Put the scheme on a base controller, so every API controller gets it:</p>
<pre><code class="language-csharp">[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public abstract class ApiControllerBase : ControllerBase { }
</code></pre>
<p>This also fixes the most common bug in mixed apps. With the cookie scheme, an unauthenticated request gets a <strong>302 redirect to the login page</strong>. A mobile app expects a <strong>401</strong>, and gets an HTML login page instead. With the JWT scheme on the API controllers, the challenge returns a clean 401.</p>
<h2>4. Issue tokens with the same Identity users</h2>
<p>The token endpoint checks the password with Identity's <code>SignInManager</code>, then builds a token with the user's roles:</p>
<pre><code class="language-csharp">[AllowAnonymous]
[HttpPost("/api/Token")]
public async Task&lt;IActionResult&gt; Token(LoginRequest request)
{
    var user = await userManager.FindByNameAsync(request.UserName);
    if (user is null) return Unauthorized();

    var result = await signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
    if (!result.Succeeded) return Unauthorized();

    var claims = new List&lt;Claim&gt;
    {
        new(JwtRegisteredClaimNames.Sub, user.Id),
        new(ClaimTypes.Name, user.UserName!)
    };
    foreach (var role in await userManager.GetRolesAsync(user))
        claims.Add(new Claim(ClaimTypes.Role, role));

    var creds = new SigningCredentials(
        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!)),
        SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: config["Jwt:Issuer"],
        audience: config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddHours(2),
        signingCredentials: creds);

    return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
</code></pre>
<p>Note <code>lockoutOnFailure: true</code>. A token endpoint is a password-guessing target, so it should respect the same lockout rules as the login page.</p>
<blockquote>
<p><strong>See the API:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core REST API in eCommerceMVCCore</a> covers catalog, cart, orders, PayPal and account endpoints, with a Postman collection.</p>
</blockquote>
<h2>5. Share the services, not the controllers</h2>
<p>The MVC <code>CartController</code> and the API <code>CartController</code> are thin. Both call the same <code>ICartService</code>, <code>IOrderService</code> and pricing code. The rules for prices, stock and promo codes live in one place, so the website and the app can't disagree about what an order costs.</p>
<h2>Things to get right before production</h2>
<ul>
<li><strong>The signing key.</strong> Use a long random secret, kept out of <code>appsettings.json</code> in source control. Environment variables or a secret store are both fine. A placeholder key in production means anyone can mint admin tokens.</li>
<li><strong>Token lifetime.</strong> Short-lived tokens limit the damage of a leaked one. If you need long sessions in the app, add refresh tokens rather than making access tokens last for weeks.</li>
<li><strong>CORS.</strong> A native mobile app doesn't need CORS, but a browser-based client on another domain does. Allow only the origins you actually use.</li>
</ul>
<h2>Testing it</h2>
<p>I ship a Postman collection with the project. The first request calls <code>POST /api/Token</code> and saves the token to a variable, and every other request sends it as <code>Authorization: Bearer ...</code>. It's the quickest way to check that the API returns 401s, not login pages.</p>
<h2>FAQ</h2>
<h3>Can ASP.NET Core use cookie and JWT authentication at the same time?</h3>
<p>Yes. Register both schemes, keep the cookie as the default, and name the JWT scheme on your API controllers. For more complex rules, a policy scheme with <code>ForwardDefaultSelector</code> can choose the scheme per request.</p>
<h3>Why does my ASP.NET Core API return a login page instead of 401?</h3>
<p>Because the cookie scheme's challenge is a redirect to the login page. Authenticate API controllers with the JWT bearer scheme, and its challenge returns a plain 401.</p>
<h3>Where should I store the JWT signing key?</h3>
<p>In environment variables, a secret store or a key vault, never in source control. Use a long random key: at least 256 bits for HS256. The go-live checklist in <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> includes replacing the placeholder key.</p>
<h2>Get the source code</h2>
<p>This setup, with catalog, cart, order, PayPal and account endpoints, is part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 source code with the JWT REST API</a>.</p>
<p>Do you keep your API in the same project as the website, or split it out? Tell me in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[ASP.NET Core localization: language URLs (/en, /ar) and RTL layouts]]></title><description><![CDATA[Build multilingual ASP.NET Core MVC sites with culture-prefixed URLs, RouteDataRequestCultureProvider, right-to-left Arabic layouts and hreflang SEO.]]></description><link>https://rgk.hashnode.dev/asp-net-core-localization-language-urls-en-ar-and-rtl-layouts</link><guid isPermaLink="true">https://rgk.hashnode.dev/asp-net-core-localization-language-urls-en-ar-and-rtl-layouts</guid><category><![CDATA[C#]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:15:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/90314187-e83e-43e3-b039-da8beeb19522.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My ASP.NET Core e-commerce project, <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a>, sells in English, Arabic, French, Spanish and Turkish. Early on, the language lived in a cookie. It worked for shoppers, but search engines saw a single URL with five different languages behind it, and a shared link opened in whatever language the recipient had last used.</p>
<p>Putting the language in the URL fixed both problems: <code>/en/products</code>, <code>/ar/products</code>, <code>/fr/products</code>. Here's the setup, with simplified code.</p>
<h2>1. Add the culture to the route</h2>
<p>Make the culture the first route segment, with a constraint so random words don't match:</p>
<pre><code class="language-csharp">app.MapControllerRoute(
    name: "localized",
    pattern: "{culture:regex(^(en|ar|fr|es|tr)$)}/{controller=Home}/{action=Index}/{id?}");

// "/" on its own redirects to the default language
app.MapGet("/", () =&gt; Results.Redirect("/en"));
</code></pre>
<h2>2. Read the culture from the route</h2>
<p>ASP.NET Core ships a <code>RouteDataRequestCultureProvider</code>. Put it first, so the URL wins over cookies and the browser's <code>Accept-Language</code> header:</p>
<pre><code class="language-csharp">var cultures = new[] { "en", "ar", "fr", "es", "tr" };

builder.Services.Configure&lt;RequestLocalizationOptions&gt;(options =&gt;
{
    options.SetDefaultCulture("en")
           .AddSupportedCultures(cultures)
           .AddSupportedUICultures(cultures);

    options.RequestCultureProviders.Insert(0, new RouteDataRequestCultureProvider
    {
        RouteDataStringKey = "culture",
        UIRouteDataStringKey = "culture"
    });
});

// after UseRouting, so route values are available
app.UseRequestLocalization();
</code></pre>
<p>One catch: the culture provider needs route values, so <code>UseRequestLocalization</code> has to run after <code>UseRouting</code>. If the culture is always <code>en</code> no matter the URL, check the middleware order first.</p>
<h2>3. Keep the culture in every generated link</h2>
<p>Once the culture is a route value, the tag helpers carry it along automatically for links within the same request. <code>asp-controller="Products" asp-action="Details"</code> on an <code>/ar/...</code> page produces an <code>/ar/...</code> link. For a language switcher, set it explicitly:</p>
<pre><code class="language-html">&lt;a asp-route-culture="fr" asp-controller="Home" asp-action="Index"&gt;Français&lt;/a&gt;
</code></pre>
<h2>4. Flip the layout for right-to-left languages</h2>
<p>.NET already knows which cultures are right-to-left. Use it in the layout instead of hard-coding "ar":</p>
<pre><code class="language-cshtml">@using System.Globalization
@{
    var ui = CultureInfo.CurrentUICulture;
    var dir = ui.TextInfo.IsRightToLeft ? "rtl" : "ltr";
}
&lt;html lang="@ui.TwoLetterISOLanguageName" dir="@dir"&gt;
</code></pre>
<p>Then load an RTL build of your CSS framework when <code>dir</code> is <code>rtl</code>. Bootstrap has an RTL stylesheet, and the bigger job is your own CSS. Anything written as <code>margin-left</code> or <code>float: right</code> needs a second look. CSS logical properties such as <code>margin-inline-start</code> flip on their own and save a lot of overrides.</p>
<blockquote>
<p><strong>See it live:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">multilingual ASP.NET Core store</a> switches between <code>/en</code> and <code>/ar</code> with a full right-to-left layout.</p>
</blockquote>
<h2>5. Tell search engines about the other languages</h2>
<p>Add <code>hreflang</code> links to each page so Google shows the right language in each country:</p>
<pre><code class="language-cshtml">@foreach (var c in new[] { "en", "ar", "fr", "es", "tr" })
{
    &lt;link rel="alternate" hreflang="@c"
          href="@Url.Action(null, null, new { culture = c }, Context.Request.Scheme)" /&gt;
}
</code></pre>
<h2>Things that tripped me up</h2>
<ul>
<li><strong>Numbers and prices.</strong> <code>ToString("C")</code> uses the current culture, so a price can suddenly show with a comma as the decimal separator. Decide whether currency formatting follows the language or the store's currency, and format explicitly.</li>
<li><strong>Model binding.</strong> Decimal inputs are parsed with the current culture too. A French user typing <code>12,50</code> and an English user typing <code>12.50</code> both need to work, so test your forms in each language.</li>
<li><strong>Icons with direction.</strong> Arrows and "next" chevrons should mirror in RTL. A simple <code>[dir=rtl] .icon-arrow { transform: scaleX(-1); }</code> handles most of them.</li>
</ul>
<h2>Translations from the database</h2>
<p>I keep the UI strings in the database, not in <code>.resx</code> files, so translations can be edited and imported from the admin dashboard without a redeploy. That's a separate article, but the routing above works the same either way.</p>
<h2>FAQ</h2>
<h3>How do I add multiple languages to an ASP.NET Core MVC app?</h3>
<p>Register localization, configure <code>RequestLocalizationOptions</code> with your supported cultures, choose how the culture is picked (here, from the route), and serve translated strings from <code>.resx</code> files or a database.</p>
<h3>Is a URL prefix, a subdomain or a cookie better for SEO?</h3>
<p>A URL prefix or a subdomain. Search engines need a separate URL for each language; a cookie hides the other languages from crawlers. A prefix such as <code>/fr</code> is the simplest to run on one domain.</p>
<h3>Does Bootstrap support right-to-left layouts?</h3>
<p>Yes. Bootstrap 5 ships <code>bootstrap.rtl.min.css</code>, and Bootstrap 4 has separate RTL builds. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> uses Bootstrap with an RTL stylesheet for Arabic.</p>
<h2>Get the source code</h2>
<p>All of this, including five ready language packs and full RTL support, is part of <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the multilingual ASP.NET Core 10 e-commerce source code</a>.</p>
<p>Do you put the language in the URL, a subdomain or a cookie? I'd like to hear what worked for you.</p>
]]></content:encoded></item><item><title><![CDATA[PayPal integration in ASP.NET Core: a safe checkout flow with Orders v2]]></title><description><![CDATA[Integrate PayPal Checkout in ASP.NET Core with the Orders v2 API: server-side totals, capture before saving, automatic refunds and verified webhooks.]]></description><link>https://rgk.hashnode.dev/paypal-integration-in-asp-net-core-a-safe-checkout-flow-with-orders-v2</link><guid isPermaLink="true">https://rgk.hashnode.dev/paypal-integration-in-asp-net-core-a-safe-checkout-flow-with-orders-v2</guid><category><![CDATA[dotnet]]></category><category><![CDATA[asp.net core]]></category><category><![CDATA[C#]]></category><category><![CDATA[Databases]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:12:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/dc05d246-0ea4-43f5-a55d-49af0172f101.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Adding a PayPal button takes an afternoon. Making sure you never lose money, never ship unpaid orders and never charge for an order you didn't save takes more care.</p>
<p>This is the PayPal checkout flow I use in <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core 10 e-commerce platform</a>, with simplified C# code for each step. It works for PayPal accounts and for debit and credit cards through PayPal.</p>
<p><img src="https://raw.githubusercontent.com/rgkgit/ecommercemvccore-blog-images/main/images/d02.png" alt="The safe PayPal flow: recalculate, create, capture, save, and refund automatically if saving fails" /></p>
<h2>1. Never trust the browser's total</h2>
<p>The browser can send any price it likes. Before creating a PayPal order, rebuild the cart on the server from product IDs and quantities, and read prices, discounts and promo codes from your database.</p>
<pre><code class="language-csharp">var lines = await cartService.GetLinesAsync(userId);          // product IDs + quantities
var total = await pricingService.CalculateTotalAsync(lines);  // prices from the database
var paypalOrder = await payPal.CreateOrderAsync(total, currency);
return Ok(new { id = paypalOrder.Id });
</code></pre>
<h2>2. Capture first</h2>
<p>After the shopper approves the payment, capture it on the server with PayPal's Orders API (<code>POST /v2/checkout/orders/{id}/capture</code>). Only a successful capture means you were paid.</p>
<pre><code class="language-csharp">var capture = await payPal.CaptureOrderAsync(orderId);
if (capture.Status != "COMPLETED")
    return BadRequest("Payment was not completed.");
</code></pre>
<h2>3. Save the order only after a successful capture</h2>
<p>If you save the order before capturing, a failed payment leaves an unpaid order in your dashboard. So save it after the capture:</p>
<pre><code class="language-csharp">try
{
    await orderService.CreatePaidOrderAsync(userId, lines, capture.CaptureId);
}
catch (Exception ex)
{
    logger.LogError(ex, "Order save failed after capture {CaptureId}", capture.CaptureId);
    await payPal.RefundCaptureAsync(capture.CaptureId); // POST /v2/payments/captures/{id}/refund
    return Problem("We couldn't save your order, so your payment has been refunded.");
}
</code></pre>
<h2>4. Refund automatically if saving fails</h2>
<p>That <code>catch</code> block is the important part. If the database is down or stock ran out at the last second, the shopper gets their money back automatically instead of waiting on a support email.</p>
<blockquote>
<p><strong>See the full flow:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">ASP.NET Core PayPal checkout in eCommerceMVCCore</a> includes one-click refunds from the admin dashboard and a Sandbox / Live switch.</p>
</blockquote>
<h2>5. Keep statuses in sync with verified webhooks</h2>
<p>Refunds and disputes can also happen in the PayPal dashboard, outside your app. Subscribe to PayPal webhooks, and verify every webhook's signature with PayPal (<code>POST /v1/notifications/verify-webhook-signature</code>) before you change an order's status. Without verification, anyone can post fake "payment completed" events to your endpoint.</p>
<h2>6. Test in Sandbox</h2>
<p>Keep a Sandbox / Live switch in your settings, with separate credentials for each. Test the whole flow, including the refund path, in Sandbox. A quick way to test the refund is to make the order save throw on purpose.</p>
<h2>Summary</h2>
<ol>
<li>Recalculate the total on the server.</li>
<li>Capture the payment.</li>
<li>Save the order after the capture.</li>
<li>Refund automatically if the save fails.</li>
<li>Verify webhooks before trusting them.</li>
</ol>
<h2>FAQ</h2>
<h3>Should I use the PayPal JavaScript SDK or a server-side integration?</h3>
<p>Both. The JavaScript SDK renders the buttons and handles the buyer's approval. Creating and capturing the order happens on your server through the Orders v2 API, so the server, not the browser, decides the amount.</p>
<h3>Can customers pay by card without a PayPal account?</h3>
<p>Yes. PayPal's debit and credit card option lets guests pay by card without signing up. On the server, you create and capture the order exactly as you do for a PayPal account payment.</p>
<h3>How do I test PayPal payments in ASP.NET Core?</h3>
<p>Create a Sandbox app in the PayPal developer dashboard, use its credentials and Sandbox buyer accounts, and test the refund path as well as the happy path. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> has a Sandbox / Live switch in its settings for exactly this.</p>
<h2>Get the source code</h2>
<p>This flow, plus one-click refunds from the admin dashboard, is built into <strong>eCommerceMVCCore</strong>. <a href="https://rgkjobber.gumroad.com/l/zkcwon">Get the ASP.NET Core 10 e-commerce source code with PayPal checkout</a>.</p>
<p>How do you handle the "paid but not saved" case in your apps? Let me know in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[EF Core with SQL Server, PostgreSQL, MySQL and SQLite in one codebase]]></title><description><![CDATA[Switch EF Core database providers with one config value in ASP.NET Core 10: per-provider migrations, decimal precision, UTC dates and case-sensitive.]]></description><link>https://rgk.hashnode.dev/ef-core-with-sql-server-postgresql-mysql-and-sqlite-in-one-codebase</link><guid isPermaLink="true">https://rgk.hashnode.dev/ef-core-with-sql-server-postgresql-mysql-and-sqlite-in-one-codebase</guid><category><![CDATA[dotnet]]></category><category><![CDATA[C#]]></category><category><![CDATA[c sharp]]></category><category><![CDATA[# csharp  # beginners  # dotnet  # programming]]></category><category><![CDATA[efcore]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[GANESH KUMAR]]></dc:creator><pubDate>Wed, 14 Oct 2026 18:07:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6acfb29a828a397dd6b35217/b6b8e9da-cb09-45c1-866c-bf5feb6481b5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I moved <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore, my ASP.NET Core e-commerce project</a>, to .NET 10, I wanted buyers to use whichever database they already run. Agencies often have a SQL Server licence, startups like PostgreSQL, cheap hosting usually means MySQL, and SQLite is perfect for a quick local try.</p>
<p>Here's how I made the provider a single config value, and the pitfalls I hit along the way.</p>
<h2>Pick the provider from configuration</h2>
<p>The app reads one setting, <code>DatabaseProvider</code>, and registers the right EF Core provider. A simplified version:</p>
<pre><code class="language-csharp">var provider = builder.Configuration["DatabaseProvider"] ?? "SqlServer";
var connectionString = builder.Configuration.GetConnectionString(provider);

builder.Services.AddDbContext&lt;AppDbContext&gt;(options =&gt;
{
    switch (provider)
    {
        case "SqlServer":  options.UseSqlServer(connectionString); break;
        case "PostgreSQL": options.UseNpgsql(connectionString);    break;
        case "MySQL":      options.UseMySQL(connectionString);     break; // MySql.EntityFrameworkCore
        case "Sqlite":     options.UseSqlite(connectionString);    break;
        default: throw new InvalidOperationException($"Unknown DatabaseProvider '{provider}'.");
    }
});
</code></pre>
<p>Fail loudly on an unknown value. A typo that silently falls back to another database is much harder to debug.</p>
<h2>Keep migrations per provider</h2>
<p>Each provider generates different SQL, so one set of migrations can't serve all four. Keep a separate migrations set for each provider (for example in separate folders or assemblies), and apply the right one at startup:</p>
<pre><code class="language-csharp">using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService&lt;AppDbContext&gt;();
db.Database.Migrate();
// then seed demo data if the database is empty
</code></pre>
<p>Running migrations and seeding on first start means a buyer can clone, run and see a working store without a manual setup step.</p>
<h2>Pitfalls I hit</h2>
<p><strong>Decimal precision.</strong> Prices need an explicit precision, or each provider picks its own default:</p>
<pre><code class="language-csharp">modelBuilder.Entity&lt;Product&gt;()
    .Property(p =&gt; p.Price)
    .HasPrecision(18, 2);
</code></pre>
<p><strong>SQLite and decimals.</strong> SQLite has no native decimal type. EF Core can't translate some decimal operations, such as ordering or summing in SQL, so test your sorting and totals on SQLite specifically.</p>
<p><strong>PostgreSQL and DateTime.</strong> Npgsql maps <code>DateTime</code> to <code>timestamp with time zone</code> and expects UTC values. Store <code>DateTime.UtcNow</code> everywhere and convert for display.</p>
<p><strong>Case sensitivity.</strong> PostgreSQL string comparisons are case-sensitive by default, while SQL Server's usual collation is not. A search box that works on SQL Server can miss results on PostgreSQL, so normalise the case in your search queries.</p>
<blockquote>
<p><strong>Try it yourself:</strong> the <a href="https://rgkjobber.gumroad.com/l/zkcwon">complete ASP.NET Core store</a> ships with all four EF Core providers configured and a migration set for each.</p>
</blockquote>
<h2>Docker makes it easy to test all four</h2>
<p>I added one Docker Compose profile per database, so testing a provider is one command:</p>
<pre><code class="language-bash">docker compose --profile postgres up --build   # or sqlserver / mysql / sqlite
</code></pre>
<p>Each database container has a healthcheck, and the app waits for it before migrating. That removed the "app started before the database was ready" errors.</p>
<h2>Was it worth it?</h2>
<p>Supporting four databases costs some extra testing on every change. In return, the project fits into whatever stack a client already has, which matters a lot when you sell source code.</p>
<h2>FAQ</h2>
<h3>Can one EF Core DbContext support multiple database providers?</h3>
<p>Yes. The same <code>DbContext</code> and model work with every provider; you choose the provider at startup. What can't be shared is the migrations, because each provider generates different SQL, so keep one migrations set per provider.</p>
<h3>Which database is best for an ASP.NET Core e-commerce site?</h3>
<p>SQL Server fits teams already on Azure or Windows. PostgreSQL is a strong free choice for production. MySQL is what most budget hosting offers. SQLite is ideal for local development and demos, but not for a busy production store.</p>
<h3>How do I switch EF Core from SQL Server to PostgreSQL?</h3>
<p>Install <code>Npgsql.EntityFrameworkCore.PostgreSQL</code>, call <code>UseNpgsql</code> instead of <code>UseSqlServer</code>, generate PostgreSQL migrations, and store every <code>DateTime</code> in UTC. <a href="https://rgkjobber.gumroad.com/l/zkcwon">eCommerceMVCCore</a> does this with a single <code>DatabaseProvider</code> setting.</p>
<h2>Get the source code</h2>
<p>The full project is <strong>eCommerceMVCCore</strong>, a production-ready ASP.NET Core 10 e-commerce platform with a storefront, admin dashboard, REST API and PayPal payments. <a href="https://rgkjobber.gumroad.com/l/zkcwon">See the live demo, documentation and source code here</a>.</p>
<p>Questions about any of the pitfalls above? Ask in the comments.</p>
]]></content:encoded></item></channel></rss>