Appearance
Are you an LLM? You can read better optimized documentation at /core_features/tag_helpers.md for this page in Markdown format
Tag helpers
This document provides a technical overview of the custom server-side Tag Helpers used within the TheExampleApp application. Tag Helpers are a key feature of ASP.NET Core used to encapsulate server-side logic for creating and rendering HTML elements in Razor files. They enable cleaner, more maintainable view code by abstracting complex generation logic into reusable components.
Custom tag helpers
In TheExampleApp, custom tag helpers are used to provide enhanced HTML generation capabilities, primarily for rendering content sourced from the Contentful CMS. By using tag helpers, we maintain a strong separation of concerns, keeping our Razor views (.cshtml) declarative and free from complex C# code.
All custom tag helpers within the application are made globally available to all views via a directive in the _ViewImports.cshtml file. This file is processed by the Razor engine before rendering any view in its directory or subdirectories.
csharp
// File: /Pages/_ViewImports.cshtml
@namespace TheExampleApp.Pages
@using Microsoft.AspNetCore.Http;
@using Microsoft.AspNetCore.Mvc.Localization
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Contentful.AspNetCore
@addTagHelper *, TheExampleApp // This line registers all custom tag helpers in our assembly
@inject IViewLocalizer Localizer
@inject TheExampleApp.Configuration.IContentfulOptionsManager Manager1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
The @addTagHelper *, TheExampleApp directive instructs the Razor engine to discover and enable all classes inheriting from TagHelper within the TheExampleApp assembly.
MarkdownTagHelper
The MarkdownTagHelper is the primary custom tag helper in this project. Its sole purpose is to convert Markdown text into HTML at render time. This is a critical component of our content strategy, as it allows content to be authored in Markdown within Contentful and seamlessly rendered as rich HTML in the browser. This is a core part of the application's Content Display functionality.
Purpose: Rendering Markdown to HTML
This helper processes a string of Markdown text and replaces it with its corresponding HTML representation. It leverages the powerful Markdig library (version 0.15.4) to perform the conversion, ensuring compliance with the CommonMark specification and support for various extensions.
Implementation using Markdig
The helper is designed to be flexible and can be invoked in two ways: as a custom element (<markdown>) or as an attribute on a standard HTML element (<div markdown>). This is configured via HtmlTargetElement attributes on the class.
csharp
// File: /TagHelpers/MarkdownTagHelper.cs
using Markdig;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;
namespace TheExampleApp.TagHelpers
{
/// <summary>
/// Taghelper for turning markdown into html.
/// </summary>
[HtmlTargetElement("markdown")]
[HtmlTargetElement(Attributes = "markdown")]
public class MarkdownTagHelper : TagHelper
{
/// <summary>
/// The model expression for which property to convert.
/// </summary>
public ModelExpression Content { get; set; }
/// <summary>
/// Asynchronously executes the taghelper with the given context and output.
/// </summary>
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// If used as <markdown>, suppress the parent tag from rendering.
if (output.TagName == "markdown")
{
output.TagName = null;
}
// Always remove the 'markdown' attribute to avoid it appearing in the final HTML.
output.Attributes.RemoveAll("markdown");
var content = await GetContent(output);
var markdown = content;
var html = Markdown.ToHtml(markdown ?? "");
output.Content.SetHtmlContent(html ?? "");
}
private async Task<string> GetContent(TagHelperOutput output)
{
if (Content == null)
return (await output.GetChildContentAsync()).GetContent();
return Content.Model?.ToString();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Key Implementation Details:
- Dual Targeting: The
[HtmlTargetElement("markdown")]and[HtmlTargetElement(Attributes = "markdown")]attributes allow the helper to activate on both a<markdown>tag and any tag with amarkdownattribute. - Content Sourcing: The
GetContentmethod provides two ways to supply the Markdown text:- Child Content: By processing the content nested inside the tag (e.g.,
<markdown># Hello</markdown>). This is the default when noContentproperty is provided. - Attribute Binding: Via the
Contentproperty, which is bound to a model expression (e.g.,content="@Model.Copy"). This is used for binding to model data.
- Child Content: By processing the content nested inside the tag (e.g.,
- Output Manipulation:
output.TagName = null;: When the helper is used as<markdown>, this line prevents the<markdown>tag itself from being rendered in the final HTML, outputting only the converted HTML.output.Attributes.RemoveAll("markdown");: This removes themarkdownattribute from the output element to keep the final HTML clean.output.Content.SetHtmlContent(html ?? "");: This replaces the original content of the tag with the newly generated HTML from Markdig.
Usage in Views
The helper is used in views to render Markdown fields from content models.
Example from LessonCopy.cshtml:
This example demonstrates binding the Copy property of the view model to the tag helper.
csharp
// File: /Views/Shared/LessonCopy.cshtml
@model LessonCopy
<div class="lesson-module lesson-module-copy">
<div class="lesson-module-copy__copy">
<markdown content="@Model.Copy"></markdown>
</div>
</div>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Alternative Usage (Attribute):
This example shows how the helper can be used as an attribute on a standard div element, processing the child content within it.
html
<div markdown class="my-content">
# This is a title
And this is a paragraph of text that will be converted to HTML.
</div>1
2
3
4
5
2
3
4
5
Testing
The MarkdownTagHelper is fully unit-tested to ensure its correctness and to verify both targeting modes. The tests can be found in TheExampleApp.Tests/TagHelpers/MarkdownTagHelperTests.cs. These tests use mock TagHelperContext and TagHelperOutput objects to simulate the Razor engine's behavior and assert the final HTML output. For more details on the testing strategy, see the Unit Testing documentation.
csharp
// File: /TheExampleApp.Tests/TagHelpers/MarkdownTagHelperTests.cs
[Fact]
public async Task MarkdownTagWithChildContentShouldReturnRenderedMarkdown()
{
//Arrange
var helper = new MarkdownTagHelper();
var context = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString());
var output = new TagHelperOutput("markdown", new TagHelperAttributeList(), GetChildContent("## Banana"));
//Act
await helper.ProcessAsync(context, output);
//Assert
Assert.Null(output.TagName); // Verifies the <markdown> tag is removed
Assert.StartsWith("<h2>Banana</h2>", output.Content.GetContent());
}
[Fact]
public async Task DivWithAttributeShouldReturnRenderedMarkdown()
{
//Arrange
var helper = new MarkdownTagHelper();
var context = new TagHelperContext(new TagHelperAttributeList { new TagHelperAttribute("markdown") }, new Dictionary<object, object>(), Guid.NewGuid().ToString());
var output = new TagHelperOutput("div", new TagHelperAttributeList { new TagHelperAttribute("markdown") }, GetChildContent("# Mr French"));
//Act
await helper.ProcessAsync(context, output);
//Assert
Assert.Equal("div", output.TagName); // Verifies the <div> tag is preserved
Assert.StartsWith("<h1>Mr French</h1>", output.Content.GetContent());
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Creating custom tag helpers
Developers can create new tag helpers to encapsulate other reusable view logic. The following guidelines should be followed.
TagHelper base class
All custom tag helpers must inherit from the Microsoft.AspNetCore.Razor.TagHelpers.TagHelper base class and should override either the Process or ProcessAsync method.
csharp
public class MyCustomTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
// Synchronous processing logic here...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
Processing attributes
Public properties on a tag helper class are automatically populated from attributes of the same name (converted from kebab-case to PascalCase) in the Razor view.
- Define a public property: To accept an attribute, define a public property on your class.
- Use
[HtmlAttributeName]: If the attribute name in HTML should be different from the property name (e.g., to support names with hyphens), use the[HtmlAttributeName("my-attribute-name")]attribute. - Target Elements: Use
[HtmlTargetElement("tag-name", Attributes = "required-attribute")]to control precisely which elements your helper will run on.
Async processing
For any operation that may involve I/O (like database calls, API requests, or reading file content), it is critical to use the asynchronous ProcessAsync method to avoid blocking threads.
- Inherit from
TagHelperand overridepublic override async Task ProcessAsync(...). - Use
awaitfor any asynchronous calls within the method, such asawait output.GetChildContentAsync(). - The
MarkdownTagHelperserves as a canonical example of a correctly implemented asynchronous tag helper.