Ever received a confirmation email that looked so professional you wondered how it was generated? What if you could craft beautiful, dynamic email templates with the same polish—all within your ASP.NET Core application?
If you’re looking to master .NET email
handling with modern tooling, this series is for you. In this first part, we’ll dive into the art of templating emails using Scriban, a blazing-fast templating engine, and show how to send them reliably with SendPulse SMTP. This is not just another SMTP tutorial—we’ll focus on real-world development practices that are scalable, maintainable, and elegant.
🔥 Why Email Templating Matters in ASP.NET Core
Before we jump into code, let’s quickly understand why templating is vital:
- Consistency: Templates ensure uniformity in design and branding.
- Maintainability: Easily update structure/content in one place.
- Personalization: Dynamically inject data like names, dates, or order details.
- Separation of Concerns: Cleanly separates design (HTML template) from logic (C# backend).
With tools like Scriban, we can create templated HTML emails that are dynamic, efficient, and developer-friendly.
💡 Scriban vs Razor vs String Templates
Let’s compare some templating options you might be familiar with:
Feature | Scriban | Razor Pages | String Replacement |
---|---|---|---|
Performance | 🚀 Very Fast | Moderate | Fast |
Security (no code) | ✅ Safe | ❌ Risky with logic | ❌ Risky |
Syntax | Simple, Clean | ASP.NET style | Primitive |
Use-case Fit | ❌ Not suitable |
Scriban is tailor-made for scenarios where you want pure templating without executable code—making it ideal for email generation.
Why Scriban and SendPulse? (Spoiler: It’s About Control)
Most tutorials settle for Razor or string-based templates. But here’s the reality:
- Razor is overkill for emails (heavy, server-bound).
- String concatenation is unmaintainable.
- Third-party SaaS locks you into pricing tiers.
Scriban solves this: a lightweight, secure, text-based templating engine with Liquid-like syntax. It decouples design from code, letting marketers edit templates without redeploys.
SendPulse (unlike free Gmail/Outlook SMTP) offers:
- High deliverability: Dedicated IPs and reputation management.
- Analytics: Open/click tracking (critical for transactional emails).
- Generous free tier: 15,000 emails/month.
💡 Real Talk: Using free SMTP for production is like sending postcards—they get lost. SendPulse’s SMTP is your certified mail.
🛠️ Getting Started – Setting Up the Project
Let’s walk through creating a sample .NET email
solution:
Step 1: Install Scriban
dotnet add package Scriban
dotnet add package SendPulse.Net
Crafting Templates with Scriban: Your Design Sanctuary
Create /Templates/Emails/welcome-email.sbnhtml
:
<!-- Templates/Emails/welcome-email.sbnhtml -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=display-width, initial-scale=1.0, maximum-scale=1.0," />
<title>Thank you</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i" rel='stylesheet' type='text/css' />
<style type="text/css">
html {
width: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
-webkit-text-size-adjust: none;
-ms-text-size-adjust: none;
}
img {
display: block !important;
border: 0;
-ms-interpolation-mode: bicubic;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
line-height: 100%;
}
.MsoNormal {
font-family: 'Open Sans', Arial, Helvetica Neue, Helvetica, sans-serif !important;
}
p {
margin: 0 !important;
padding: 0 !important;
}
.images {
display: block !important;
width: 100% !important;
}
.display-button td, .display-button a {
font-family: 'Open Sans', Arial, Helvetica Neue, Helvetica, sans-serif !important;
}
.display-button a:hover {
text-decoration: none !important;
}
/* MEDIA QUIRES */
@@media only screen and (min-width:799px) {
.main-width {
width: 600px;
}
.width680 {
width: 680px !important;
max-width: 680px !important;
}
.saf-table {
display: table !important;
}
}
@media only screen and (max-width:799px) {
body
{
width: auto !important;
}
.display-width {
width: 100% !important;
}
.display-width-inner {
width: 600px !important;
}
.padding {
padding: 0 20px !important;
}
.width680 {
width: 100% !important;
max-width: 100% !important;
}
}
@media only screen and (max-width:639px) {
body
{
width: auto !important;
}
.display-width {
width: 100% !important;
}
.display-width-inner, .display-width-child {
width: 100% !important;
}
.display-width-child .button-width .display-button {
width: auto !important;
}
.padding {
padding: 0 20px !important;
}
.div-width {
display: block !important;
width: 100% !important;
max-width: 100% !important;
}
.butn-center {
display: table !important;
margin: 0 auto !important;
}
.width-auto {
width: 100% !important;
}
.hide-height, .hide-bar {
display: none !important;
}
}
@media only screen and (max-width:480px) {
.display-width table, .display-width-child2 table
{
width: 100% !important;
}
.display-width .button-width .display-button {
width: auto !important;
}
.div-width {
display: block !important;
width: 100% !important;
max-width: 100% !important;
}
.width-auto {
width: 100% !important;
max-width: 100% !important;
}
}
@media only screen and (max-width:380px) {
.display-width table
{
width: 100% !important;
}
.display-width .button-width .display-button {
width: auto !important;
}
}
</style>
</head>
<body>
<!--[if mso]>
<style>
.heading {font-family: Arial, Helvetica Neue, Helvetica, sans-serif !important;}
.MsoNormal {font-family: Arial, Helvetica Neue, Helvetica, sans-serif !important;}
.display-button td, .display-button a, a {font-family: Arial, Helvetica Neue, Helvetica, sans-serif !important;}
.width-auto {
width:auto !important;
}
</style>
<![endif]-->
<!-- MENU STARTS -->
<table align="center" bgcolor="#F2F4F7" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td height="80" style="line-height:80px; mso-line-height-rule:exactly;"> </td>
</tr>
<tr>
<td align="center">
<!--[if mso]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="680" style="width: 680px;">
<tr>
<td align="center" valign="top" width="680">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:680px; vertical-align:top;" class="width680">
<!-- ID:BG MENU -->
<table align="center" bgcolor="#ffffff" border="0" cellpadding="0" cellspacing="0" class="display-width" width="100%" style="max-width:680px;">
<tr>
<td align="center" class="padding">
<!--[if mso]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="600" style="width: 600px;">
<tr>
<td align="center" valign="top" width="600">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:600px; vertical-align:top;" class="main-width">
<table align="center" border="0" class="display-width-inner" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;">
<tr>
<td height="15" style="line-height:15px; mso-line-height-rule:exactly;"> </td>
</tr>
<tr>
<td align="center">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%; max-width:100%;">
<tr>
<td align="left" style="width:100%; max-width:100%;">
<!--[if mso]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="100%" style="width:100%;">
<tr>
<td align="center" valign="top" width="150">
<![endif]-->
<div style="display:inline-block; max-width:150px; width:100%; vertical-align:top;" class="div-width">
<!--TABLE LEFT-->
<table align="left" border="0" cellpadding="0" cellspacing="0" class="display-width-child" width="100%" style="border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; width:100%; max-width:100%;">
<tr>
<td align="center">
<table align="center" border="0" cellpadding="0" cellspacing="0" style="width:auto !important;">
<tr>
<!-- ID:TXT MENU -->
<td align="center">
<a href="#" style="color:#333333; text-decoration:none;"><img src="https://softdevbytes.com/wp-content/uploads/2025/06/cropped-softdev-byte-logo-204-50-1.png" alt="Logo" height="40" style="margin:0; border:0; padding:0; display:block;" /></a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="15" style="line-height:15px; mso-line-height-rule:exactly;"> </td>
</tr>
<tr>
<td bgcolor="#1270ce" height="2"></td>
</tr>
<tr>
<td height="15" style="line-height:15px; mso-line-height-rule:exactly;"> </td>
</tr>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
<!-- MENU ENDS -->
<!-- REGISTRATION STARTS -->
<table align="center" bgcolor="#F2F4F7" border="0" cellpadding="0" cellspacing="0" width="100%">
<tbody>
<tr>
<td align="center">
<!--[if mso]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="680" style="width: 680px;">
<tr>
<td align="center" valign="top" width="100%" style="max-width:680px;">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:680px; vertical-align:top;" class="width680">
<!-- ID:BG SECTION-1 -->
<table align="center" bgcolor="#ffffff" border="0" cellpadding="0" cellspacing="0" class="display-width" width="100%" style="max-width:680px;">
<tbody>
<tr>
<td align="center" class="padding">
<!--[if mso]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="600" style="width:600px;">
<tr>
<td align="center">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:600px; vertical-align:top;" class="main-width">
<table align="left" border="0" class="display-width-inner" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;">
<tr>
<td height="30" style="mso-line-height-rule:exactly; line-height:30px; font-size:0;"> </td>
</tr>
<tr>
<!-- ID:TXT TITLE -->
<td align="left" class="MsoNormal" style="color:#333333; font-family:Segoe UI, Helvetica Neue, Arial, Verdana, Trebuchet MS, sans-serif; font-weight:400; font-size:14px; line-height:36px; letter-spacing:1px;">
Welcome, {{name}} !
</td>
</tr>
<tr>
<td height="5" style="mso-line-height-rule:exactly; line-height:5px; font-size:0;"> </td>
</tr>
<tr>
<td height="20" style="mso-line-height-rule:exactly; line-height:20px; font-size:0;"> </td>
</tr>
<tr>
<td align="left">
<table align="left" border="0" cellpadding="0" cellspacing="0" width="90%" style="border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; width:90%; max-width:90%;">
<tr>
<!-- ID:TXT CONTENT -->
<td align="left" class="MsoNormal" style="color:#666666; font-family:Segoe UI, Helvetica Neue, Arial, Verdana, Trebuchet MS, sans-serif; font-weight:400; font-size:14px; line-height:36px;letter-spacing:1px;">
Kindly verify your account by clicking on the button below.
</td>
</tr>
<tr>
<td height="20" style="mso-line-height-rule: exactly; line-height:20px; font-size:0;"> </td>
</tr>
<tr>
<td align="center" class="button-width">
<!-- ID:BTN COMMON BUTTON -->
<table align="center" bgcolor="#F2F4F7" border="0" cellpadding="0" cellspacing="0" class="display-button" style="border-radius:5px;">
<tr>
<td align="center" class="MsoNormal" style="font-family:Segoe UI, Arial, Verdana, Trebuchet MS, sans-serif; font-weight:600; padding:10px 12px; font-size:20px; letter-spacing:1px;">
<a style="text-decoration:none" href="{{activationLink}}">Confirm your email </a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="mso-line-height-rule: exactly; line-height:20px; font-size:0;"> </td>
</tr>
<tr>
<td align="center" class="MsoNormal" style="color:#666666;font-family:'Segoe UI',sans-serif,Arial,Helvetica,Lato;font-size:14px;line-height:24px;">
<span class="txt-content">If you didn’t initiate this, kindly ignore this mail</span>
</td>
</tr>
<tr>
<td height="15" style="mso-line-height-rule: exactly; line-height:15px; font-size:0;"> </td>
</tr>
<tr>
<td align="center" class="MsoNormal" style="color:#666666;font-family:'Segoe UI',sans-serif,Arial,Helvetica,Lato;font-size:14px;line-height:24px;">
<span class="txt-content">Please contact</span> <span class="txt-link-content" style="color:#7F56D9; "><a href="#" style="color:#7F56D9; ">Support</a></span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="30" style="mso-line-height-rule:exactly; line-height:30px; font-size:0;"> </td>
</tr>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
<!-- REGISTRATION ENDS -->
<!-- FOOTER STARTS -->
<table align="center" bgcolor="#F2F4F7" border="0" cellpadding="0" cellspacing="0" width="100%">
<tbody>
<tr>
<td align="center">
<!--[if (gte mso 9)|(IE)]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="680" style="width: 680px;">
<tr>
<td align="center" valign="top" width="100%" style="max-width:680px;">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:680px; vertical-align:top;" class="width680">
<!-- ID:BG FOOTER -->
<table align="center" bgcolor="#1270ce" border="0" cellpadding="0" cellspacing="0" class="display-width" width="100%" style="max-width:680px;">
<tbody>
<tr>
<td align="center" class="padding">
<!--[if (gte mso 9)|(IE)]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="600" style="width:600px;">
<tr>
<td align="center">
<![endif]-->
<div style="display:inline-block; width:100%; max-width:600px; vertical-align:top;" class="main-width">
<table align="center" border="0" class="display-width-inner" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;">
<tr>
<td height="20" style="mso-line-height-rule:exactly; line-height:20px; font-size:0;"> </td>
</tr>
<tr>
<td align="center">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="width:100%; max-width:100%;">
<tr>
<td align="center" style="width:100%; max-width:100%; font-size:0;">
<!--[if (gte mso 9)|(IE)]>
<table aria-hidden="true" border="0" cellspacing="0" cellpadding="0" align="center" width="100%" style="width:100%;">
<tr>
<td align="center" valign="top" width="250">
<![endif]-->
<div style="display:inline-block; max-width:250px; width:100%; vertical-align:top;" class="div-width">
<!--TABLE LEFT-->
<table align="center" border="0" cellpadding="0" cellspacing="0" class="display-width-child" width="100%" style="border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; width:100%; max-width:100%;">
<tbody>
<tr>
<td align="center" style="padding:5px 0">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<!-- ID:TXT COPYRIGHT -->
<td align="center" class="MsoNormal" style="color:#ffffff; font-family:Segoe UI, Helvetica Neue, Arial, Verdana, Trebuchet MS, sans-serif; font-size:14px; line-height:24px; letter-spacing:1px;">
Copyright © 2025, SoftDevBytes
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if (gte mso 9)|(IE)]>
</td>
</table>
<![endif]-->
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td height="20" style="mso-line-height-rule:exactly; line-height:20px; font-size:0;"> </td>
</tr>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
<tr>
<td height="100" style="line-height:80px; mso-line-height-rule:exactly;"> </td>
</tr>
</tbody>
</table>
<!-- FOOTER ENDS -->
</body>
</html>
Key Advantages:
- Logic (
if/for
) without C# code in templates. - Filters (
date.to_string
) for formatting. - No recompilation needed: Change templates on the fly.
The finished Email Template

Configuring SendPulse: The Delivery Workhorse
- Sign up for a SendPulse account.
- Grab your SMTP credentials:
Add sendpulse sdk configuration to program.cs
:
builder.Services.AddSendPulseNet(config =>
{
config.BaseUrl = "https://api.sendpulse.com";
config.ClientId = "991e5dcb9163ca4d5d**********";
config.ClientSecret = "aq6hjckmnhf991e5dcb91*********";
});
The Email Template Service:
using Microsoft.AspNetCore.Hosting;
using Scriban;
public interface ITemplateParserService
{
/// <summary>
/// Parses the specified template file and replaces placeholders using the provided model.
/// </summary>
/// <param name="templateFileName">Template file name with extension</param>
/// <param name="model">An anonymous or strongly typed object</param>
/// <returns>Parsed string content</returns>
Task<string> ParseTemplate(string templateFileName,object model);
}
public class TemplateService(IWebHostEnvironment environment): ITemplateParserService
{
private const string EmailTemplateFolder = "Templates/Emails";
/// <summary>
/// Parses the specified template file and replaces placeholders using the provided model.
/// </summary>
/// <param name="templateFileName">Template file name with extension</param>
/// <param name="model">An anonymous or strongly typed object</param>
/// <returns>Parsed string content</returns>
public async Task<string> ParseTemplate(string templateFileName, object model)
{
string templatePath = Path.Combine(environment.ContentRootPath, EmailTemplateFolder,templateFileName);
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"Template file not found: {templatePath}");
}
string templateContent = await File.ReadAllTextAsync(templatePath);
var template = Template.Parse(templateContent);
if (template.HasErrors)
{
string errors = string.Join("\n", template.Messages.Select(m => m.Message));
throw new InvalidOperationException($"Template parsing failed:\n{errors}");
}
string emailBody = await template.RenderAsync(model);
return emailBody;
}
}
The Email Sending Service: Code That Scales
// Services/EmailService.cs
using EmailMastery.Models;
using SendPulseNetSDK.SendPulse;
using SendPulseNetSDK.SendPulse.Models;
namespace EmailMastery.Services;
public interface IEmailService
{
Task<bool> SendWelcomeEmailAsync(string recipientEmail, string recipientName, string confirmationLink);
}
internal sealed class EmailService(ITemplateParserService templateParser, ISendPulseClient sendPulseClient) : IEmailService
{
public async Task<bool> SendWelcomeEmailAsync(string recipientEmail, string recipientName, string confirmationLink)
{
// 1. Load Scriban template
string mailHtmlBody = await templateParser.ParseTemplate("welcome-email.sbnhtml", new WelcomeEmailModel
{
Name = recipientName,
Link = confirmationLink
});
// 2. Create email message
var senderEmail = new EmailAddress() { Email = "postmaster@nassipartnerships.com", Name = "SoftDevBytes" };
var toEmail = new List<EmailAddress>() { new EmailAddress() { Email = recipientEmail, Name = recipientName } };
try
{
var emailResponse = await sendPulseClient.SendApiEmailAsync(senderEmail, toEmail, "Welcome Email", mailHtmlBody);
if (emailResponse == null) return false;
return emailResponse.Result;
}
catch (Exception e)
{
return false;
}
}
}
Welcome Email Template Model Class
//Models/WelcomeEmailModel
namespace EmailMastery.Models;
public class WelcomeEmailModel
{
public string Name { get; set; }
public string Link { get; set; }
}
Email Endpoint
app.MapGet("/email",async (IEmailService emailService) =>
{
var response = await emailService.SendWelcomeEmailAsync("myemail@gmail.com","Mark", "https://softdevbytes.com/confirm?token=12345");
if (response)
{
return Results.Ok("Email sent successfully!");
}
return Results.BadRequest("Failed to send email. Please try again later.");
})
.WithName("EmailSender");
Your Turn: Build, Test, Deploy!
Stuck? Inspired? I want to hear from you! Leave a comment below:
- What email challenges are you facing?
- What features should we cover in Part 2?
👉 Subscribe to get the next installment instantly. No spam.
“Your inbox is a sacred space. Treat it that way.” — Unknown DevOps Sage