DNTCaptcha.Core in ASP.NET Core: Complete CAPTCHA Guide

Quick answer: DNTCaptcha.Core is an open-source NuGet package that adds a self-hosted image CAPTCHA to ASP.NET Core MVC and Razor Pages forms. You install the package, register it with AddDNTCaptcha(), render it with the <dnt-captcha> tag helper, and validate it on the server with the [ValidateDNTCaptcha] attribute. No third-party account or external script is required.
This guide walks through a working setup for a registration form, then covers the parts most tutorials skip: choosing a storage provider, running on more than one server, accessibility, rate limiting, and how to debug the errors developers usually hit.
In this guide
- When Should You Use a CAPTCHA?
- Prerequisites
- Step 1: Install the Package
- Step 2: Register the Service
- Step 3: Enable the Tag Helper
- Step 4: Render the CAPTCHA in the Form
- Step 5: Validate on the Server
- Choose the Right Storage Provider
- Running on More Than One Server
- Accessibility and User Experience
- Add Rate Limiting Alongside the CAPTCHA
- Troubleshooting
- DNTCaptcha.Core vs Hosted CAPTCHA Services
- Frequently Asked Questions
When Should You Use a CAPTCHA?
A CAPTCHA adds friction for every real user, so add one only where automated abuse is likely:
- Registration and password-reset forms that bots use to create accounts or flood inboxes.
- Contact and comment forms that attract spam.
- Login pages after several failed attempts (rather than on every login).
- Expensive endpoints, such as forms that send SMS messages or run heavy reports.
A CAPTCHA is one layer, not a complete defence. Combine it with server-side validation, anti-forgery tokens, rate limiting and account lockout. DNTCaptcha.Core is a good fit when you want everything self-hosted, for example on an intranet, in a region where third-party scripts are blocked, or when you prefer not to send visitor data to an external service.
Prerequisites
- An ASP.NET Core MVC or Razor Pages project. If you are new to the framework, start with our ASP.NET Core tutorial for beginners.
- A form that already posts to the server and uses model validation.
- A .NET version supported by the package release you install. Check the package page on NuGet before upgrading.
Step 1: Install the Package
dotnet add package DNTCaptcha.CorePin the version in your .csproj and review the release notes on the project’s GitHub page when you upgrade, because option names occasionally change between major versions.
Step 2: Register the Service
Add the service in Program.cs. The storage provider decides where the expected answer is kept between rendering the form and submitting it:
using DNTCaptcha.Core;
builder.Services.AddDNTCaptcha(options =>
{
options.UseCookieStorageProvider(SameSiteMode.Strict)
.AbsoluteExpiration(minutes: 7)
.ShowThousandsSeparators(false);
});Seven minutes is a reasonable expiry: long enough for someone who types slowly or uses assistive technology, short enough that a captured answer is not useful for long.
Step 3: Enable the Tag Helper
Add this line to Views/_ViewImports.cshtml (MVC) or Pages/_ViewImports.cshtml (Razor Pages):
@addTagHelper *, DNTCaptcha.CoreStep 4: Render the CAPTCHA in the Form
<form asp-action="Register" method="post">
<div asp-validation-summary="ModelOnly"></div>
<label asp-for="Email"></label>
<input asp-for="Email" />
<label asp-for="Password"></label>
<input asp-for="Password" />
<dnt-captcha
asp-captcha-generator-max="99999"
asp-captcha-generator-min="11111"
asp-captcha-generator-language="English"
asp-captcha-generator-display-mode="ShowDigits"
asp-validation-error-message="Enter the CAPTCHA correctly."
asp-placeholder="Enter the number shown"
asp-use-relative-urls="true" />
<button type="submit">Create account</button>
</form>The tag helper outputs the image, a refresh button, hidden token fields and the text box. Keep asp-use-relative-urls="true" when the app runs behind a reverse proxy, otherwise the image URL may point to the internal host name.
Step 5: Validate on the Server
In MVC, decorate the POST action. The attribute adds a model error when the answer is wrong, so your normal ModelState check handles it:
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateDNTCaptcha(
ErrorMessage = "Enter the CAPTCHA correctly.",
CaptchaGeneratorLanguage = Language.English,
CaptchaGeneratorDisplayMode = DisplayMode.ShowDigits)]
public IActionResult Register(RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Create the account only after every check has passed.
return RedirectToAction("Success");
}The language and display mode in the attribute must match the values used in the tag helper. In Razor Pages, inject IDNTCaptchaValidatorService and call its validation method inside OnPost instead. The method signature differs between package versions, so check the README for the version you installed.
Choose the Right Storage Provider
| Provider | How it works | Use it when |
|---|---|---|
| Cookie | The expected value is encrypted into a cookie on the visitor’s browser | Single server, or several servers that share Data Protection keys |
| Session | The value is kept in ASP.NET Core session state | Session is already configured in the app |
| Memory cache | The value is kept in the server’s memory | Exactly one server instance |
| Distributed cache | The value is kept in Redis, SQL Server or another IDistributedCache | Load-balanced or containerised deployments |
Start with cookie storage. Move to a distributed cache if you scale out and do not want to depend on shared encryption keys.
Running on More Than One Server
The most common production bug is “works locally, fails after deployment”. With cookie storage, the CAPTCHA value is protected by ASP.NET Core Data Protection. Each server generates its own keys by default, so a CAPTCHA rendered by server A cannot be decrypted by server B, and validation fails at random behind a load balancer.
Fix it by sharing the key ring across instances and giving the app a fixed name:
using Microsoft.AspNetCore.DataProtection;
builder.Services.AddDataProtection()
.SetApplicationName("MyWebApp")
.PersistKeysToFileSystem(new DirectoryInfo(@"\\fileserver\keys\MyWebApp"));Redis, Azure Blob Storage and a database are also supported key stores. The same fix applies to authentication cookies and anti-forgery tokens, which use Data Protection too.
Accessibility and User Experience
Image CAPTCHAs are hard for screen-reader users and people with low vision. Reduce the impact:
- Show the CAPTCHA only where abuse is real, or only after a failed attempt.
- Use digits rather than distorted letters, and keep the font size readable.
- Make sure the refresh button and text box have clear labels and a sensible tab order.
- Keep the expiry long enough that a slow user does not get an “expired” error.
- Offer another contact route, such as an email address, for anyone who cannot complete it.
The package also supports display modes that ask the user to type the number in words or solve a small sum. Test each mode with real users before choosing one, because a harder puzzle mostly blocks people, not bots.
Add Rate Limiting Alongside the CAPTCHA
A CAPTCHA does not stop someone from hammering your endpoint. ASP.NET Core includes rate-limiting middleware (in .NET 7 and later) that you can apply to the same form:
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("forms", o =>
{
o.PermitLimit = 5;
o.Window = TimeSpan.FromMinutes(1);
});
});
var app = builder.Build();
app.UseRateLimiter();
// MVC: put [EnableRateLimiting("forms")] on the controller action.This fixed window limits each policy to five requests per minute. For per-user or per-IP limits, use a partitioned limiter keyed on the user ID or remote IP address.
Troubleshooting
The CAPTCHA image does not load
Open the browser’s developer tools and check the image request. A 404 usually means routing or a path base issue behind a proxy; set asp-use-relative-urls="true" and confirm the app’s path base. A 500 error is often a missing font or graphics dependency on Linux containers, so check the server logs for the exception.
Validation always fails
Check that the language and display mode in [ValidateDNTCaptcha] match the tag helper, that the hidden token fields are inside the same <form>, and that the cookie is not blocked by a SameSite or HTTPS setting. Look at the request in the network tab to confirm the CAPTCHA fields are being posted.
It works locally but fails on several servers
Share Data Protection keys or switch to a distributed cache storage provider, as described above. Also confirm all instances run the same package version.
DNTCaptcha.Core vs Hosted CAPTCHA Services
| Option | Hosting | Main trade-off |
|---|---|---|
| DNTCaptcha.Core | Self-hosted in your app | Full control and no external calls, but image puzzles are weaker against modern OCR |
| Google reCAPTCHA | Hosted by Google | Strong bot scoring, but loads Google scripts and needs a privacy notice |
| Cloudflare Turnstile | Hosted by Cloudflare | Usually invisible to users, requires a Cloudflare account |
| hCaptcha | Hosted by hCaptcha | Privacy-focused alternative with visible challenges |
For a low-traffic intranet form, DNTCaptcha.Core plus rate limiting is often enough. For a public sign-up page that attracts targeted bots, a hosted service with behavioural scoring is usually stronger.
Frequently Asked Questions
Does DNTCaptcha.Core work with Razor Pages?
Yes. Rendering uses the same tag helper. For validation, inject the validator service into the page model and check the result in OnPost.
Is DNTCaptcha.Core free?
Yes. It is an open-source package published on NuGet. Review its licence and maintenance activity on GitHub before using it in a commercial project.
Can bots solve image CAPTCHAs?
Some can, using OCR or paid solving services. That is why a CAPTCHA should be combined with rate limiting, anti-forgery tokens, account lockout and monitoring.
For more .NET topics, see our programming and development guide.