When to build your own form
Umbraco Forms is a great commercial package that lets editors build forms themselves. If you only need one or two fixed forms, such as a contact form, a few classes of your own are free and give you full control. That's how the quote forms on umbraco.ninja work.
Step 1: The form model
using System.ComponentModel.DataAnnotations;
namespace MySite.Controllers;
public class ContactFormModel
{
[Required, StringLength(100)]
public string Name { get; set; } = string.Empty;
[Required, EmailAddress, StringLength(200)]
public string Email { get; set; } = string.Empty;
[Required, StringLength(4000)]
public string Message { get; set; } = string.Empty;
// Honeypot: hidden from people, often filled in by bots.
public string? Website { get; set; }
}Step 2: The surface controller
A surface controller handles form posts from Umbraco pages. This one validates the form, sends an email, and redirects to show a thank-you message:
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Website.Controllers;
namespace MySite.Controllers;
public class ContactController : SurfaceController
{
private readonly IEmailSender _emailSender;
private readonly GlobalSettings _globalSettings;
private readonly ILogger<ContactController> _logger;
public ContactController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IEmailSender emailSender,
IOptions<GlobalSettings> globalSettings,
ILogger<ContactController> logger)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_emailSender = emailSender;
_globalSettings = globalSettings.Value;
_logger = logger;
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(ContactFormModel model)
{
if (string.IsNullOrEmpty(model.Website) == false)
{
return RedirectToCurrentUmbracoPage(); // quietly ignore bots
}
if (ModelState.IsValid == false)
{
return CurrentUmbracoPage();
}
string body = $"""
<p><strong>Name:</strong> {WebUtility.HtmlEncode(model.Name)}</p>
<p><strong>Email:</strong> {WebUtility.HtmlEncode(model.Email)}</p>
<p>{WebUtility.HtmlEncode(model.Message).Replace("\n", "<br>")}</p>
""";
var message = new EmailMessage(
from: _globalSettings.Smtp?.From,
to: "hello@example.com",
subject: $"Website enquiry from {model.Name}",
body: body,
isBodyHtml: true);
try
{
await _emailSender.SendAsync(message, emailType: "ContactForm");
}
catch (Exception ex)
{
_logger.LogError(ex, "Contact form email could not be sent");
ModelState.AddModelError(string.Empty, "Sorry, your message couldn't be sent. Please try again later.");
return CurrentUmbracoPage();
}
TempData["ContactSent"] = true;
return RedirectToCurrentUmbracoPage();
}
}[ValidateAntiForgeryToken]blocks cross-site request forgery.BeginUmbracoFormadds the token for you.- Everything the visitor typed is HTML-encoded before it goes into the email, so nobody can inject links or scripts.
RedirectToCurrentUmbracoPage()after success stops the form being re-sent if the visitor refreshes.
Step 3: The form partial
@using MySite.Controllers
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContactFormModel>
@if (TempData["ContactSent"] is true)
{
<p class="success">Thanks, we'll be in touch soon.</p>
}
else
{
using (Html.BeginUmbracoForm<ContactController>(nameof(ContactController.Submit)))
{
@Html.ValidationSummary(true)
<label asp-for="Name">Your name</label>
<input asp-for="Name" autocomplete="name">
<span asp-validation-for="Name"></span>
<label asp-for="Email">Email</label>
<input asp-for="Email" type="email" autocomplete="email">
<span asp-validation-for="Email"></span>
<label asp-for="Message">Message</label>
<textarea asp-for="Message" rows="6"></textarea>
<span asp-validation-for="Message"></span>
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input asp-for="Website" tabindex="-1" autocomplete="off">
</div>
<button type="submit">Send message</button>
}
}Step 4: Put it on a page
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@using MySite.Controllers
<h1>@Model.Name</h1>
@await Html.PartialAsync("ContactForm", new ContactFormModel())Email needs SMTP set up first. See How to send email from Umbraco.
Going further
- Save submissions as well as emailing them: create a content node, or write to your own database table.
- Rate-limit the form by IP address to slow down spam bots.
- Never email passwords or card numbers. If a form must collect sensitive data, encrypt it at rest instead.
Need a hand?
umbraco.ninja has more than 20 years of Umbraco experience. If you'd rather not do this yourself, we'll set it up, test it and hand it over.
Get a fixed-price quote