<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Steven Kamwaza</title>
    <description>The latest articles on DEV Community by Steven Kamwaza (@stevenkamwaza).</description>
    <link>https://dev.to/stevenkamwaza</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044864%2F00e2f32f-7185-4fc7-b652-212acb9d4d25.png</url>
      <title>DEV Community: Steven Kamwaza</title>
      <link>https://dev.to/stevenkamwaza</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stevenkamwaza"/>
    <language>en</language>
    <item>
      <title>Stop Wiring Quill by Hand: RazorRichText for ASP.NET Core</title>
      <dc:creator>Steven Kamwaza</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:54:00 +0000</pubDate>
      <link>https://dev.to/stevenkamwaza/stop-wiring-quill-by-hand-razorrichtext-for-aspnet-core-3p2h</link>
      <guid>https://dev.to/stevenkamwaza/stop-wiring-quill-by-hand-razorrichtext-for-aspnet-core-3p2h</guid>
      <description>&lt;p&gt;Adding a rich text box to an ASP.NET Core app usually means three separate jobs: embed Quill, keep the HTML out of XSS trouble, and teach model validation that &lt;code&gt;&amp;lt;p&amp;gt;&amp;lt;br&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt; is empty. &lt;a href="https://www.nuget.org/packages/RazorRichText" rel="noopener noreferrer"&gt;RazorRichText&lt;/a&gt; is a MIT-licensed NuGet package that does those jobs for you.&lt;/p&gt;

&lt;p&gt;You bind a string with &lt;code&gt;asp-for&lt;/code&gt;. On POST you get sanitised HTML. The editor itself is a tag helper, so the view stays markup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package RazorRichText
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It targets ASP.NET Core on .NET 10 and ships Quill plus the editor CSS and JS as embedded assets. You do not host those files yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it once
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Program.cs&lt;/code&gt; needs two calls: register the services, then map the embedded files. &lt;code&gt;UseEmbeddedAssets&lt;/code&gt; is on by default, so the map call is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;RazorRichText.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;RazorRichText.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllersWithViews&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRazorRichText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultHeight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultPlaceholder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Start writing…"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultPreset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ToolbarPreset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SanitizeByDefault&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CleanPasteFromWord&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpsRedirection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRouting&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseAuthorization&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapRazorRichTextAssets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// serves /_razor-rich-text/*.css and *.js&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapControllerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{controller=Home}/{action=Index}/{id?}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer config over code? Bind the &lt;code&gt;RazorRichText&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRazorRichText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RazorRichText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DefaultHeight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DefaultTheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DefaultPlaceholder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Start writing…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SanitizeByDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CleanPasteFromWord"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MaxContentLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;Views/_ViewImports.cshtml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@using RazorRichText.Models
@using RazorRichText.Helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, RazorRichText
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;Views/Shared/_Layout.cshtml&lt;/code&gt;, inside &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Html.RenderRichTextAssets()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call that helper once in the layout. If the toolbar paints but formatting does nothing, Quill was loaded twice. Set &lt;code&gt;AutoIncludeScripts = false&lt;/code&gt; and keep the single layout call.&lt;/p&gt;

&lt;h2&gt;
  
  
  A model that understands HTML
&lt;/h2&gt;

&lt;p&gt;A normal &lt;code&gt;[Required]&lt;/code&gt; or &lt;code&gt;[StringLength]&lt;/code&gt; attribute counts tags. A bold word is longer than the same word in plain text, and an untouched Quill editor posts &lt;code&gt;&amp;lt;p&amp;gt;&amp;lt;br&amp;gt;&amp;lt;/p&amp;gt;&lt;/code&gt;, which is not empty to &lt;code&gt;[Required]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mark the property with &lt;code&gt;[RichText]&lt;/code&gt;. That tells the binder to sanitise the value before your action runs. Length rules measure the plain text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.ComponentModel.DataAnnotations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;RazorRichText.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;RazorRichText.Services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleForm&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;StringLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RichText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RequiredRichText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ErrorMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Write the article before publishing."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MinRichTextLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MaxRichTextLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Display&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Body"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RichText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MaxRichTextLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;280&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The view is one element per field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@model ArticleForm

&amp;lt;form method="post"&amp;gt;
    @Html.AntiForgeryToken()

    &amp;lt;label asp-for="Title"&amp;gt;&amp;lt;/label&amp;gt;
    &amp;lt;input asp-for="Title" /&amp;gt;
    &amp;lt;span asp-validation-for="Title"&amp;gt;&amp;lt;/span&amp;gt;

    &amp;lt;label asp-for="Body"&amp;gt;&amp;lt;/label&amp;gt;
    &amp;lt;rich-text-editor asp-for="Body"
                      rte-preset="Blog"
                      rte-height="420"
                      rte-placeholder="Type / for a heading, list, or quote." /&amp;gt;
    &amp;lt;span asp-validation-for="Body"&amp;gt;&amp;lt;/span&amp;gt;

    &amp;lt;button type="submit"&amp;gt;Publish&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller stays a normal MVC action. &lt;code&gt;model.Body&lt;/code&gt; is already cleaned when it arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValidateAntiForgeryToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ArticleForm&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;ModelState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// model.Body is sanitised HTML. Store it.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;RedirectToAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the binder if you want that behaviour on every &lt;code&gt;[RichText]&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllersWithViews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelBinderProviders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RichTextModelBinderProvider&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the binder, inject &lt;code&gt;IHtmlSanitizerService&lt;/code&gt; and call &lt;code&gt;Sanitize&lt;/code&gt; yourself. The result tells you whether anything was stripped, which is useful in an audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a preset instead of a button list
&lt;/h2&gt;

&lt;p&gt;The default chrome is slash-first: type &lt;code&gt;/&lt;/code&gt; for commands, and a small bubble appears when text is selected. Presets choose which commands exist, so a comment box and a docs editor do not share one giant toolbar.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;What the author gets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Comment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bold, italic, underline, strikethrough, links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Chat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Comment, plus lists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Blog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Headings, lists, links, images, quotes, undo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blog, plus alignment, code, find and replace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Every group, including tables, colours, source view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A comment field that stays out of the way until it is focused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;rich-text-editor asp-for="Comment"
                  rte-preset="Comment"
                  rte-toolbar="Slash"
                  rte-appearance="Ghost"
                  rte-height="140"
                  rte-status-bar="false"
                  rte-placeholder="Add a comment…" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Ghost&lt;/code&gt; draws the border only while the editor is focused. &lt;code&gt;Plain&lt;/code&gt; is unstyled chrome for apps that already have a design system. &lt;code&gt;Themed&lt;/code&gt; is the default look (light, dark, or high contrast via &lt;code&gt;rte-theme&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A documentation editor with the classic ribbon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;rich-text-editor asp-for="Body"
                  rte-preset="Full"
                  rte-toolbar="Standard"
                  rte-appearance="Themed"
                  rte-theme="Dark"
                  rte-height="480"
                  rte-sticky-toolbar="true" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;#&lt;/code&gt; followed by space still becomes a heading, and pasting from Word is cleaned up (&lt;code&gt;CleanPasteFromWord&lt;/code&gt; defaults to true). You can ignore the presets and pass groups directly: &lt;code&gt;rte-toolbar-groups="TextFormatting,Lists,Links"&lt;/code&gt;. Use the flag name only — &lt;code&gt;All&lt;/code&gt;, not &lt;code&gt;ToolbarGroups.All&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show the saved article
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Html.Raw&lt;/code&gt; on stored HTML is fine only after sanitising. The display helper is the safer default, and it can build a table of contents from the headings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Html.RichTextDisplay(Model.Body, cssClass: "prose", showToc: true, printFriendly: true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listing pages rarely want the full HTML. Inject &lt;code&gt;IContentConverter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IContentConverter&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Excerpt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetExcerpt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;160&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountWords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;AsMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HtmlToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same service converts Markdown back to HTML, or strips tags to plain text, when you already store one format and need another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drafts and images
&lt;/h2&gt;

&lt;p&gt;Autosave writes to &lt;code&gt;localStorage&lt;/code&gt; under &lt;code&gt;rte_autosave_{editorId}&lt;/code&gt; and restores the draft when the server has no value yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;rich-text-editor asp-for="Body"
                  rte-preset="Blog"
                  rte-autosave="true"
                  rte-autosave-interval="15000" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Images need an endpoint that accepts &lt;code&gt;multipart/form-data&lt;/code&gt; (&lt;code&gt;file&lt;/code&gt;) and returns &lt;code&gt;{ "url": "/uploads/….jpg" }&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;rich-text-editor asp-for="Body"
                  rte-preset="Blog"
                  rte-allow-images="true"
                  rte-image-url="/api/images/upload" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/images"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImageUploadController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IWebHostEnvironment&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"upload"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IFormFile&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/gif"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"image/webp"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Unsupported image."&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebRootPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"uploads"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;()}{&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyToAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"/uploads/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the content type on the server. The editor will insert whatever URL you return.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need a hook, not a new editor
&lt;/h2&gt;

&lt;p&gt;Each instance is on &lt;code&gt;window.__rte&lt;/code&gt; by its id (&lt;code&gt;rte_&lt;/code&gt; plus the property name). &lt;code&gt;rte-onchange&lt;/code&gt; and &lt;code&gt;rte-onready&lt;/code&gt; name global functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;rich-text-editor asp-for="Body" rte-onchange="onBodyChange" rte-onready="onEditorReady" /&amp;gt;
&amp;lt;p id="words"&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;script&amp;gt;
  function onEditorReady(quill) { quill.focus(); }

  function onBodyChange(html, quill) {
    const words = quill.getText().trim().split(/\s+/).filter(Boolean).length;
    document.getElementById("words").textContent = words + " words";
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional modules stay off until you point at them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rte-mention-url&lt;/code&gt; for &lt;code&gt;@&lt;/code&gt; lookups&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rte-ai-url&lt;/code&gt; for a rewrite endpoint&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rte-collab-url&lt;/code&gt; plus &lt;code&gt;app.MapHub&amp;lt;RichTextHub&amp;gt;("/hubs/rte")&lt;/code&gt; for a SignalR broadcast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Snippets (heading, quote, code block) ship with &lt;code&gt;IRichTextSnippetProvider&lt;/code&gt;. Replace that service if &lt;code&gt;/&lt;/code&gt; should insert your own blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blazor
&lt;/h2&gt;

&lt;p&gt;The same package exposes a component. Render the assets from the host page, map &lt;code&gt;MapRazorRichTextAssets()&lt;/code&gt;, and bind a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;RichTextEditor @bind-Value="article.Body"
                Preset="ToolbarPreset.Blog"
                Appearance="EditorAppearance.Themed"
                Height="360"
                Placeholder="Start typing…" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ValueChanged&lt;/code&gt; fires with the HTML string, same as the MVC hidden field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three mistakes that waste an afternoon
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The tag helper is unknown.&lt;/strong&gt; &lt;code&gt;_ViewImports.cshtml&lt;/code&gt; is missing &lt;code&gt;@addTagHelper *, RazorRichText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CS0103: The name 'Blog' does not exist&lt;/code&gt;.&lt;/strong&gt; The view is missing &lt;code&gt;@using RazorRichText.Models&lt;/code&gt;. Attribute strings such as &lt;code&gt;rte-preset="Blog"&lt;/code&gt; do not need the using; C# expressions such as &lt;code&gt;rte-preset="@ToolbarPreset.Blog"&lt;/code&gt; do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The editor area is blank and the console says Quill is not loaded.&lt;/strong&gt; &lt;code&gt;MapRazorRichTextAssets()&lt;/code&gt; was never called, or &lt;code&gt;RenderRichTextAssets()&lt;/code&gt; is not in the layout. Embedded files are served from &lt;code&gt;/_razor-rich-text&lt;/code&gt; only after that map.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you stop maintaining
&lt;/h2&gt;

&lt;p&gt;You stop copying Quill boilerplate between projects, writing a sanitiser allow-list, and special-casing the empty-editor HTML in every view model. One package, one tag, and the POST payload is HTML you can store.&lt;/p&gt;

&lt;p&gt;Source and license: &lt;a href="https://github.com/StevenKamwaza/RazorRichText" rel="noopener noreferrer"&gt;github.com/StevenKamwaza/RazorRichText&lt;/a&gt; (MIT).&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>csharp</category>
      <category>blazor</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Steven Kamwaza</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:40:38 +0000</pubDate>
      <link>https://dev.to/stevenkamwaza/-5gmf</link>
      <guid>https://dev.to/stevenkamwaza/-5gmf</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5" class="crayons-story__hidden-navigation-link"&gt;Twinify 1.1.0: Map Without Profiles, Hide Secrets With Attributes, and Ship Native AOT&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/stevenkamwaza" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044864%2F00e2f32f-7185-4fc7-b652-212acb9d4d25.png" alt="stevenkamwaza profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/stevenkamwaza" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Steven Kamwaza
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Steven Kamwaza
                
                
              
              &lt;div id="story-author-preview-content-4655344" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/stevenkamwaza" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4044864%2F00e2f32f-7185-4fc7-b652-212acb9d4d25.png" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Steven Kamwaza&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5" id="article-link-4655344"&gt;
          Twinify 1.1.0: Map Without Profiles, Hide Secrets With Attributes, and Ship Native AOT
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/twinify"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;twinify&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/csharp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;csharp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Twinify 1.1.0: Map Without Profiles, Hide Secrets With Attributes, and Ship Native AOT</title>
      <dc:creator>Steven Kamwaza</dc:creator>
      <pubDate>Tue, 15 Sep 2026 05:11:32 +0000</pubDate>
      <link>https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5</link>
      <guid>https://dev.to/stevenkamwaza/twinify-110-map-without-profiles-hide-secrets-with-attributes-and-ship-native-aot-57c5</guid>
      <description>&lt;p&gt;If you missed the earlier posts on Twinify: &lt;a href="https://dev.to/stevenkamwaza/getting-started-with-twinify-a-free-open-source-object-mapper-for-modern-net-2hn3"&gt;Twinify is  a MIT-licensed object mapper for &lt;strong&gt;.NET 8, 9, and 10&lt;/strong&gt;. You call &lt;code&gt;mapper.Map&amp;lt;UserDto&amp;gt;(user)&lt;/code&gt; and it copies matching members. Profiles still exist when names are not enough&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/stevenkamwaza/twinify-102-a-look-at-the-latest-performance-improvements-lap"&gt;1.0.2 was about speed. &lt;strong&gt;1.1.0 is about less boilerplate&lt;/strong&gt;, safer DTOs, and &lt;strong&gt;Native AOT&lt;/strong&gt;.&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twinify
dotnet add package Twinify.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Twinify" rel="noopener noreferrer"&gt;Twinify&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;You still inject &lt;code&gt;IMapper&lt;/code&gt; and you still call &lt;code&gt;Map&lt;/code&gt;. Existing profile-only apps keep the same behaviour. Everything below is additive.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Mapping without a profile
&lt;/h2&gt;

&lt;p&gt;Matching names used to still need &lt;code&gt;CreateMap&lt;/code&gt;. Not anymore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwinify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMapper&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt; &lt;span class="nf"&gt;ToDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;CreateMap&lt;/code&gt;. Twinify copies same-name members, flattens &lt;code&gt;Address.City&lt;/code&gt; → &lt;code&gt;AddressCity&lt;/code&gt;, and walks nested objects and collections.&lt;/p&gt;

&lt;p&gt;No DI? Same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Mapper&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Keep your profiles. Auto-map the rest.
&lt;/h2&gt;

&lt;p&gt;Registering a profile is still classic Twinify: only declared top-level pairs map.&lt;/p&gt;

&lt;p&gt;Want both? One flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwinify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllowMappingWithoutProfiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CreateMap&lt;/code&gt; always wins for that pair. Nested types inside an explicit map still auto-map — you do not need &lt;code&gt;CreateMap&amp;lt;Address, AddressDto&amp;gt;()&lt;/code&gt; just because &lt;code&gt;User&lt;/code&gt; has an &lt;code&gt;Address&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Attributes instead of another &lt;code&gt;ForMember&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hide secrets&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IgnoreMap&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rename or flatten without a profile&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MapFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Address.City"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Town&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pick a constructor&lt;/strong&gt; when the destination has more than one: &lt;code&gt;[MapConstructor]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Need to turn attributes off? &lt;code&gt;UseMappingAttributes(false)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Native AOT: a source generator in the same package
&lt;/h2&gt;

&lt;p&gt;JIT apps still compile maps at runtime. Native AOT cannot do that.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Twinify&lt;/code&gt; package now ships &lt;strong&gt;Twinify.SourceGenerator&lt;/strong&gt;. Mark the pair; Twinify emits a static, reflection-free map at compile time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Twinify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDto&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or at assembly scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;TwinifyMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uncustomized &lt;code&gt;CreateMap&lt;/code&gt; in a profile constructor can be generated too. Nested convention-compatible members of a generated pair are included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest limitation:&lt;/strong&gt; fluent &lt;code&gt;ForMember&lt;/code&gt; / &lt;code&gt;ConvertUsing&lt;/code&gt; are &lt;strong&gt;not&lt;/strong&gt; generated. Prefer &lt;code&gt;[MapFrom]&lt;/code&gt; / &lt;code&gt;[IgnoreMap]&lt;/code&gt; on AOT, or keep those pairs on JIT. A pair with no generated map throws &lt;code&gt;MappingNotFoundException&lt;/code&gt; on Native AOT.&lt;/p&gt;

&lt;p&gt;That is the 1.0.2 “what’s next” item, shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Records, in-place map, fail at boot
&lt;/h2&gt;

&lt;p&gt;Records bind constructor parameters by name. Extra settable properties still copy after construction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;PersonDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PersonDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update an existing instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;existingDto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catch bad maps at startup instead of on the first request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwinify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateMappings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still stuck? &lt;code&gt;mapper.Explain&amp;lt;User, UserDto&amp;gt;()&lt;/code&gt; shows where each destination member gets its value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrade path
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You have&lt;/th&gt;
&lt;th&gt;You do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profile-only 1.0.x app&lt;/td&gt;
&lt;td&gt;Update the package. Behaviour stays the same.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tired of &lt;code&gt;CreateMap&lt;/code&gt; for identical DTOs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AllowMappingWithoutProfiles()&lt;/code&gt; or drop profiles for those pairs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native AOT / trimming&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;[Twinify]&lt;/code&gt; or &lt;code&gt;[TwinifyMap]&lt;/code&gt; and prefer attributes over fluent customizations.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;IMapper&lt;/code&gt; and &lt;code&gt;Map&amp;lt;TDestination&amp;gt;(source)&lt;/code&gt; did not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twinify &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.0
dotnet add package Twinify.DependencyInjection &lt;span class="nt"&gt;--version&lt;/span&gt; 1.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Twinify" rel="noopener noreferrer"&gt;https://www.nuget.org/packages/Twinify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release notes: &lt;a href="https://github.com/StevenKamwaza/Twinify.Docs/releases/tag/1.1.0" rel="noopener noreferrer"&gt;Twinify 1.1.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you map DTOs in ASP.NET Core, workers, or AOT apps, I would love a try, a star, or a comment about the pair that still feels too noisy.&lt;/p&gt;

&lt;p&gt;Twinify — object mapping without the ceremony.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>twinify</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Twinify 1.0.2: A Look at the Latest Performance Improvements</title>
      <dc:creator>Steven Kamwaza</dc:creator>
      <pubDate>Wed, 19 Aug 2026 06:15:59 +0000</pubDate>
      <link>https://dev.to/stevenkamwaza/twinify-102-a-look-at-the-latest-performance-improvements-lap</link>
      <guid>https://dev.to/stevenkamwaza/twinify-102-a-look-at-the-latest-performance-improvements-lap</guid>
      <description>&lt;p&gt;Over the past few weeks, the performance and memory efficiency of &lt;strong&gt;Twinify&lt;/strong&gt; has been improved, twinify is an open-source object-to-object mapping library for .NET.&lt;/p&gt;

&lt;p&gt;The latest release, &lt;strong&gt;Twinify 1.0.2&lt;/strong&gt;, brings some encouraging improvements compared with the initial 1.0.0 release.&lt;/p&gt;

&lt;p&gt;I also wanted to keep the benchmarking honest, so the tests include &lt;strong&gt;AutoMapper 16.2.0&lt;/strong&gt; as a reference point.&lt;/p&gt;

&lt;p&gt;Twinify is designed to make object mapping simple, while supporting common scenarios such as nested objects, collections, flattening, constructor mapping, and custom mappings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benchmark
&lt;/h2&gt;

&lt;p&gt;The benchmark uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.NET 10&lt;/li&gt;
&lt;li&gt;BenchmarkDotNet 0.15.8&lt;/li&gt;
&lt;li&gt;Windows 11&lt;/li&gt;
&lt;li&gt;Intel Core i7-1355U&lt;/li&gt;
&lt;li&gt;Twinify 1.0.0&lt;/li&gt;
&lt;li&gt;Twinify 1.0.2&lt;/li&gt;
&lt;li&gt;AutoMapper 16.2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The object graph being mapped is not just a simple flat object.&lt;/p&gt;

&lt;p&gt;The benchmark maps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ├── Address
 └── Orders
      ├── Order
      │    └── OrderItems
      └── Order
           └── OrderItems
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two scenarios are tested:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mapping a single &lt;code&gt;User&lt;/code&gt; to &lt;code&gt;UserDto&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Mapping 1,000 users to &lt;code&gt;UserDto&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Memory allocation and GC activity are also measured using BenchmarkDotNet's &lt;code&gt;MemoryDiagnoser&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Progress
&lt;/h2&gt;

&lt;p&gt;Here are the results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Twinify 1.0.0&lt;/th&gt;
&lt;th&gt;Twinify 1.0.2&lt;/th&gt;
&lt;th&gt;AutoMapper 16.2.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single User&lt;/td&gt;
&lt;td&gt;Mean&lt;/td&gt;
&lt;td&gt;3.819 μs&lt;/td&gt;
&lt;td&gt;4.087 μs&lt;/td&gt;
&lt;td&gt;1.066 μs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single User&lt;/td&gt;
&lt;td&gt;Allocated&lt;/td&gt;
&lt;td&gt;3,304 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,848 B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;848 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 Users&lt;/td&gt;
&lt;td&gt;Mean&lt;/td&gt;
&lt;td&gt;4,420.174 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,420.444 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,108.267 μs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 Users&lt;/td&gt;
&lt;td&gt;Allocated&lt;/td&gt;
&lt;td&gt;3,320,120 B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,864,120 B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;864,120 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 Users&lt;/td&gt;
&lt;td&gt;Gen0&lt;/td&gt;
&lt;td&gt;523.4375&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;296.8750&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;136.7188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 Users&lt;/td&gt;
&lt;td&gt;Gen1&lt;/td&gt;
&lt;td&gt;242.1875&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;171.8750&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;78.1250&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  45% Faster Bulk Mapping
&lt;/h2&gt;

&lt;p&gt;The biggest improvement can be seen when mapping 1,000 users.&lt;/p&gt;

&lt;p&gt;Twinify 1.0.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4,420.174 μs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twinify 1.0.2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2,420.444 μs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's approximately a &lt;strong&gt;45% reduction in execution time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The performance gap with AutoMapper has also become much smaller.&lt;/p&gt;

&lt;p&gt;AutoMapper 16.2.0 completes the same benchmark in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,108.267 μs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is still a gap, but the improvement from the initial Twinify release is significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  44% Less Memory Allocation
&lt;/h2&gt;

&lt;p&gt;Performance isn't only about execution time.&lt;/p&gt;

&lt;p&gt;For the 1,000-user benchmark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Twinify 1.0.0
3,320,120 B

Twinify 1.0.2
1,864,120 B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's approximately &lt;strong&gt;44% less memory allocation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is particularly important for applications performing large numbers of mappings because unnecessary allocations can increase GC pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduced GC Pressure
&lt;/h2&gt;

&lt;p&gt;The reduction in allocations is also reflected in the GC measurements.&lt;/p&gt;

&lt;p&gt;For the 1,000-user benchmark, Gen0 collections dropped from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;523.4375 → 296.8750
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's approximately a &lt;strong&gt;43% reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Gen1 also decreased:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;242.1875 → 171.8750
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or approximately &lt;strong&gt;29%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These improvements are encouraging because they show that the memory optimizations are not simply reflected in the allocation column; they are also reducing GC activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About AutoMapper?
&lt;/h2&gt;

&lt;p&gt;AutoMapper 16.2.0 is still ahead in this benchmark.&lt;/p&gt;

&lt;p&gt;For 1,000 users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Twinify 1.0.2   2,420.444 μs
AutoMapper      1,108.267 μs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in allocations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Twinify 1.0.2   1,864,120 B
AutoMapper        864,120 B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So there is still work to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;The next optimization targets are clear.&lt;/p&gt;

&lt;p&gt;I'm particularly interested in reducing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mapping execution overhead&lt;/li&gt;
&lt;li&gt;Allocations during nested and collection mapping&lt;/li&gt;
&lt;li&gt;Reflection or dynamic dispatch on the hot path&lt;/li&gt;
&lt;li&gt;Temporary objects and collections&lt;/li&gt;
&lt;li&gt;GC pressure during large mapping operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current results already show that there is plenty of room for optimization.&lt;/p&gt;

&lt;p&gt;The next milestone is to continue closing the gap while keeping Twinify's API simple and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Benchmark?
&lt;/h2&gt;

&lt;p&gt;One of the things I want to emphasize with Twinify is that performance claims should be backed by reproducible benchmarks.&lt;/p&gt;

&lt;p&gt;Rather than simply saying that a mapper is "fast", I'm trying to track actual changes between releases.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Twinify 1.0.0
      ↓
4.42 ms / 1,000 users
      ↓
Twinify 1.0.2
      ↓
2.42 ms / 1,000 users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives future releases something concrete to improve against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Twinify
&lt;/h2&gt;

&lt;p&gt;Twinify is available on NuGet and the documentation contains examples covering profiles, nested mappings, collections, dependency injection, and other mapping scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/StevenKamwaza/Twinify.Docs?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Twinify Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NuGet:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://www.nuget.org/packages/Twinify?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;Twinify on NuGet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The current NuGet listing identifies &lt;strong&gt;1.0.2 as the latest Twinify release&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Twinify 1.0.2 is another step forward.&lt;/p&gt;

&lt;p&gt;The current benchmark shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;45% faster&lt;/strong&gt; bulk mapping compared with Twinify 1.0.0&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;44% less memory allocation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;♻️ &lt;strong&gt;43% fewer Gen0 collections&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📈 A significantly smaller performance gap compared with the initial release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is still a lot of optimization ahead, and that's part of the fun.&lt;/p&gt;

&lt;p&gt;I'm looking forward to seeing how far Twinify can go.&lt;/p&gt;

&lt;p&gt;If you're interested in .NET object mapping, performance optimization, or open-source development, I'd love to hear your thoughts and feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Twinify — Object Mapping Made Simple.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  dotnet #csharp #opensource #performance
&lt;/h1&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>performance</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Getting Started with Twinify: A Free, Open-Source Object Mapper for Modern .NET</title>
      <dc:creator>Steven Kamwaza</dc:creator>
      <pubDate>Fri, 24 Jul 2026 05:44:07 +0000</pubDate>
      <link>https://dev.to/stevenkamwaza/getting-started-with-twinify-a-free-open-source-object-mapper-for-modern-net-2hn3</link>
      <guid>https://dev.to/stevenkamwaza/getting-started-with-twinify-a-free-open-source-object-mapper-for-modern-net-2hn3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbab5e6yd0ozahjxta014.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbab5e6yd0ozahjxta014.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lightweight. Familiar. MIT Licensed. Built for .NET 8, 9, and 10.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Following AutoMapper's recent licensing changes, many .NET developers have started looking for an open-source alternative that doesn't require rewriting years of mapping code.&lt;/p&gt;

&lt;p&gt;That's one of the reasons I built &lt;strong&gt;Twinify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Twinify is a lightweight, high-performance object-to-object mapper that provides a familiar fluent API, making it easy to map DTOs, entities, view models, records, and complex object graphs while remaining completely open source under the &lt;strong&gt;MIT License&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're already using AutoMapper, you'll find the transition straightforward for many common mapping scenarios.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Twinify?
&lt;/h1&gt;

&lt;p&gt;✔ MIT Licensed&lt;/p&gt;

&lt;p&gt;✔ Built for .NET 8, .NET 9 &amp;amp; .NET 10&lt;/p&gt;

&lt;p&gt;✔ Familiar AutoMapper-style Fluent API&lt;/p&gt;

&lt;p&gt;✔ Dependency Injection Support&lt;/p&gt;

&lt;p&gt;✔ Automatic Convention-Based Mapping&lt;/p&gt;

&lt;p&gt;✔ Nested Objects &amp;amp; Collections&lt;/p&gt;

&lt;p&gt;✔ Flattening&lt;/p&gt;

&lt;p&gt;✔ Record &amp;amp; Constructor Mapping&lt;/p&gt;

&lt;p&gt;✔ Mapping Diagnostics with &lt;code&gt;Explain&amp;lt;TSource, TDestination&amp;gt;()&lt;/code&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Installation
&lt;/h1&gt;

&lt;p&gt;Install the required packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twinify
dotnet add package Twinify.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Step 1 — Create a Mapping Profile
&lt;/h1&gt;

&lt;p&gt;Define your mappings by inheriting from &lt;code&gt;MappingProfile&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MappingProfile&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CreateMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ignore&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twinify uses a familiar fluent configuration model that should feel natural to developers coming from AutoMapper.&lt;/p&gt;




&lt;h1&gt;
  
  
  Step 2 — Register Twinify
&lt;/h1&gt;

&lt;p&gt;Register your mapping profiles during application startup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwinify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Step 3 — Map Your Objects
&lt;/h1&gt;

&lt;p&gt;Inject &lt;code&gt;IMapper&lt;/code&gt; wherever it's needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMapper&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt; &lt;span class="nf"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all you need.&lt;/p&gt;

&lt;p&gt;Twinify automatically maps properties with matching names while allowing full customization when required.&lt;/p&gt;




&lt;h1&gt;
  
  
  Features
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Automatic Convention Mapping
&lt;/h2&gt;

&lt;p&gt;Properties with identical names are mapped automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User.Name
        ↓
UserDto.Name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No configuration required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automatic Flattening
&lt;/h2&gt;

&lt;p&gt;Nested properties are flattened automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source

User.Address.City

↓

Destination

UserDto.AddressCity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Nested Object Mapping
&lt;/h2&gt;

&lt;p&gt;Twinify recursively maps nested object graphs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order
 └── Customer
 └── Address
 └── Contacts

↓

OrderDto
 └── CustomerDto
 └── AddressDto
 └── ContactDtos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Collection Mapping
&lt;/h2&gt;

&lt;p&gt;Supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lists&lt;/li&gt;
&lt;li&gt;Arrays&lt;/li&gt;
&lt;li&gt;Dictionaries&lt;/li&gt;
&lt;li&gt;Nested collections&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Constructor &amp;amp; Record Mapping
&lt;/h2&gt;

&lt;p&gt;Twinify automatically constructs destination types, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Immutable objects&lt;/li&gt;
&lt;li&gt;Records&lt;/li&gt;
&lt;li&gt;Parameterized constructors&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Fluent Configuration
&lt;/h2&gt;

&lt;p&gt;Twinify supports a rich configuration API, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CreateMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ForMember&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MapFrom&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Condition&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConvertUsing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BeforeMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AfterMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReverseMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IncludeBase&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreserveReferences&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Explain Your Mappings
&lt;/h2&gt;

&lt;p&gt;One feature that sets Twinify apart is mapping diagnostics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Explain&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of guessing why a property mapped the way it did, &lt;code&gt;Explain()&lt;/code&gt; shows exactly where every destination member receives its value, making debugging significantly easier.&lt;/p&gt;




&lt;h1&gt;
  
  
  Migrating from AutoMapper
&lt;/h1&gt;

&lt;p&gt;Many existing AutoMapper projects can migrate with only a few mechanical changes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AutoMapper&lt;/th&gt;
&lt;th&gt;Twinify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MappingProfile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreateMap()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same familiar API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ForMember()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same familiar API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MapFrom()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same familiar API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReverseMap()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AddAutoMapper()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddTwinify()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom resolvers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ConvertUsing()&lt;/code&gt; and member configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Migration Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Remove AutoMapper
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet remove package AutoMapper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Install Twinify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twinify
dotnet add package Twinify.DependencyInjection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Update Your Profiles
&lt;/h3&gt;

&lt;p&gt;Replace&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MappingProfile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. Update Dependency Injection
&lt;/h3&gt;

&lt;p&gt;Replace&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAutoMapper&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTwinify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. Build &amp;amp; Test
&lt;/h3&gt;

&lt;p&gt;Many existing &lt;code&gt;CreateMap()&lt;/code&gt; and &lt;code&gt;ForMember()&lt;/code&gt; configurations should require little or no modification.&lt;/p&gt;

&lt;p&gt;As with any migration, it's recommended to run your existing mapping tests to validate behavior.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I Built Twinify
&lt;/h1&gt;

&lt;p&gt;I wanted a mapper that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open source&lt;/li&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;li&gt;Easy to understand&lt;/li&gt;
&lt;li&gt;Familiar to existing AutoMapper users&lt;/li&gt;
&lt;li&gt;Built specifically for modern .NET&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than forcing developers to learn an entirely new API, Twinify focuses on keeping the transition intuitive while continuing to evolve with community feedback.&lt;/p&gt;




&lt;h1&gt;
  
  
  Getting Started
&lt;/h1&gt;

&lt;p&gt;📦 NuGet&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Twinify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📖 Documentation&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/StevenKamwaza/Twinify.Docs" rel="noopener noreferrer"&gt;https://github.com/StevenKamwaza/Twinify.Docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 Source Code&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/StevenKamwaza/Twinify" rel="noopener noreferrer"&gt;https://github.com/StevenKamwaza/Twinify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ If Twinify helps your project, consider giving the repository a star and sharing your feedback. Community contributions and feature requests are always welcome.&lt;/p&gt;




&lt;h1&gt;
  
  
  Happy Coding! 🚀
&lt;/h1&gt;

&lt;p&gt;Built with ❤️ for the .NET community.&lt;/p&gt;




&lt;h2&gt;
  
  
  Share Your Thoughts
&lt;/h2&gt;

&lt;p&gt;Have questions, feature ideas, or migration feedback?&lt;/p&gt;

&lt;p&gt;Open an issue or start a discussion on GitHub—I'd love to hear how you're using Twinify in your projects.&lt;/p&gt;




&lt;h1&gt;
  
  
  Trending Hashtags
&lt;/h1&gt;

&lt;h1&gt;
  
  
  dotnet #csharp #dotnet8 #dotnet9 #dotnet10 #opensource #nuget #softwareengineering #backend #webdevelopment #cleanarchitecture #developer #coding #programming #github #aspnetcore #entityframework #architecture #productivity #developers #devcommunity #100DaysOfCode #BuildInPublic #OSS #AutoMapper #Twinify
&lt;/h1&gt;

</description>
      <category>backend</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
