dot-net-email-templating-with-scriban-asp-net

Building Dynamic .Net Email Templates and Sending Emails in ASP.NET Core with Scriban and SendPulse

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:

FeatureScribanRazor PagesString Replacement
Performance🚀 Very FastModerateFast
Security (no code)✅ Safe❌ Risky with logic❌ Risky
SyntaxSimple, CleanASP.NET stylePrimitive
Use-case Fit✅ Email❌ Not suitable✅ Email

Scriban is tailor-made for scenarios where you want pure templating without executable code—making it ideal for email generation.

Learn more about Scriban


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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</td>
                                        </tr>
                                        <tr>
                                            <td bgcolor="#1270ce" height="2"></td>
                                        </tr>
										<tr>
											<td height="15" style="line-height:15px; mso-line-height-rule:exactly;">&nbsp;</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;">&nbsp;</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;">&nbsp;</td>
												</tr>
												<tr>
													<td height="20" style="mso-line-height-rule:exactly; line-height:20px; font-size:0;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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;">&nbsp;</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 &copy; 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;">&nbsp;</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;">&nbsp;</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

  1. Sign up for a SendPulse account.
  2. 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!

  1. Grab the code after support me here first.
  2. Replace credentials with your SendPulse keys.

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?

“Your inbox is a sacred space. Treat it that way.” — Unknown DevOps Sage