Goodbye Child Actions, Hello View Components

Updated May 22, 2016: Updated to match component invocations changes in ASP.NET Core RC2 / RTM

In previous versions of MVC, we used Child Actions to build reusable components / widgets that consisted of both Razor markup and some backend logic. The backend logic was implemented as a controller action and typically marked with a [ChildActionOnly] attribute. Child actions are extremely useful but as some have pointed out, it is easy to shoot yourself in the foot.

Child Actions do not exist in ASP.NET Core MVC. Instead, we are encouraged to use the new View Component feature to support this use case. Conceptually, view components are a lot like child actions but they are a lighter weight and no longer involve the lifecycle and pipeline related to a controller. Before we get into the differences, let’s take a look at a simple example.

A simple View Component

View components are made up of 2 parts: A view component class and a razor view.

To implement the view component class, inherit from the base ViewComponent and implement an Invoke or InvokeAsync method. This class can be anywhere in your project. A common convention is to place them in a ViewComponents folder. Here is an example of a simple view component that retrieves a list of articles to display in a What’s New section.

namespace MyWebApplication.ViewComponents
{
public class WhatsNewViewComponent : ViewComponent
{
private readonly IArticleService _articleService;

public WhatsNewViewComponent(IArticleService articleService)
{

_articleService = articleService;
}

public IViewComponentResult Invoke(int numberOfItems)
{

var articles = _articleService.GetNewArticles(numberOfItems);
return View(articles);
}
}
}

Much like a controller action, the Invoke method of a view component simply returns a view. If no view name is explicitly specified, the default Views\Shared\Components\ViewComponentName\Default.cshtml is used. In this case, Views\Shared\Components\WhatsNew\Default.cshtml. Note there are a ton of conventions used in view components. I will be covering these in a future blog post.

Views\Shared\Components\WhatsNew\Default.cshtml
@model IEnumerable<Article>

<h2>What's New</h2>
<ul>
@foreach (var article in Model)
{
<li><a asp-controller="Article"
asp-action="View"
asp-route-id="@article.Id">@article.Title</a></li>

}
</ul>

To use this view component, simply call @Component.InvokeAsync from any view in your application. For example, I added this to the Home/Index view:

Views\Home\Index.cshtml
<div class="col-md-3">
@await Component.InvokeAsync("WhatsNew", new { numberOfItems = 3})
</div>

The first parameter to @Component.InvokeAsync is the name of the view component. The second parameter is an object specifying the names and values of arguments matching the parmeters of the Invoke method in the view component. In this case, we specified a single int named numberOfItems, which matches the Invoke(int numberOfItems) method of the WhatsNewViewComponent class.

What's New View Component

How is this different?

So far this doesn’t really look any different from what we had with Child Actions. There are however some major differences here.

No Model Binding

With view components, parameters are passed directly to your view component when you call @Component.Invoke() or @Component.InvokeAsync() in your view. There is no model binding needed here since the parameters are not coming from the HTTP request. You are calling the view component directly using C#. No model binding means you can have overloaded Invoke methods with different parameter types. This is something you can’t do in controllers.

No Action Filters

View components don’t take part in the controller lifecycle. This means you can’t add action filters to a view component. While this might sound like a limitation, it is actually an area that caused problems for a lot of people. Adding an action filter to a child action would sometimes have unintended consequences when the child action was called from certain locations.

Not reachable from HTTP

A view component never directly handles an HTTP request so you can’t call directly to a view component from the client side. You will need to wrap the view component with a controller if your application requires this behaviour.

What is available?

Common Properties

When you inherit from the base ViewComponent class, you get access to a few properties that are very similar to controllers:

[ViewComponent]
public abstract class ViewComponent
{
protected ViewComponent();
public HttpContext HttpContext { get; }
public ModelStateDictionary ModelState { get; }
public HttpRequest Request { get; }
public RouteData RouteData { get; }
public IUrlHelper Url { get; set; }
public IPrincipal User { get; }

[Dynamic]
public dynamic ViewBag { get; }
[ViewComponentContext]
public ViewComponentContext ViewComponentContext { get; set; }
public ViewContext ViewContext { get; }
public ViewDataDictionary ViewData { get; }
public ICompositeViewEngine ViewEngine { get; set; }

//...
}

Most notably, you can access information about the current user from the User property and information about the current request from the Request property. Also, route information can be accessed from the RouteData property. You also have the ViewBag and ViewData. Note that the ViewBag / ViewData are shared with the controller. If you set ViewBag property in your controller action, that property will be available in any ViewComponent that is invoked by that controller action’s view.

Dependency Injection

Like controllers, view components also take part in dependency injection so any other information you need can simply be injected to the view component. In the example above, we injected the IArticleService that allowed us to access articles form some remote source. Anything that you could inject into a controller can also be injected into a view component.

Wrapping it up

View components are a powerful new feature for creating reusable widgets in ASP.NET Core MVC. Consider using View Components any time you have complex rendering logic that also requires some backend logic.

Complex Custom Tag Helpers in ASP.NET Core MVC

Updated May 5 2016: Updated code to work with ASP.NET Core RC2

In a previous blog post we talked about how to create a simple tag helper in ASP.NET Core MVC. In today’s post we take this one step further and create a more complex tag helper that is made up of multiple parts.

A Tag Helper for Bootstrap Modal Dialogs

Creating a modal dialog in bootstrap requires some verbose html.

Bootstrap Modal
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Modal title</h4>
</div>
<div class="modal-body">
<p>One fine body&hellip;</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

Using a tag helper here would help simplify the markup but this is a little more complicated than the Progress Bag example. In this case, we have HTML content that we want to add in 2 different places: the <div class="modal-body"></div> element and the <div class="modal-footer"></div> element.

The solution here wasn’t immediately obvious. I had a chance to talk to Taylor Mullen at the MVP Summit ASP.NET Hackathon in November and he pointed me in the right direction. The solution is to use 3 different tag helpers that can communicate with each other through the TagHelperContext.

Ultimately, we want our tag helper markup to look like this:

Bootstrap Modal using a Tag Helper
<modal title="Modal title">
<modal-body>
<p>One fine body&hellip;</p>
</modal-body>
<modal-footer>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</modal-footer>
</modal>

This solution uses 3 tag helpers: modal, modal-body and modal-footer. The contents of the modal-body tag will be placed inside the <div class="modal-body"></div> while the contents of the <modal-footer> tag will be placed inside the <div class="modal-footer"></div> element. The modal tag helper is the one that will coordinate all this.

Restricting Parents and Children

First things first, we want to make sure that <modal-body> and <modal-footer> can only be placed inside the <modal> tag and that the <modal> tag can only contain those 2 tags. To do this, we set the RestrictChildren attribute on the modal tag helper and the ParentTag property of the HtmlTargetElement attribute on the modal body and modal footer tag helpers:

[RestrictChildren("modal-body", "modal-footer")]
public class ModalTagHelper : TagHelper
{
//...
}

[HtmlTargetElement("modal-body", ParentTag = "modal")]
public class ModalBodyTagHelper : TagHelper
{
//...
}

[HtmlTargetElement("modal-footer", ParentTag = "modal")]
public class ModalFooterTagHelper : TagHelper
{
//...
}

Now if we try to put any other tag in the <modal> tag, Razor will give me a helpful error message.

Restrict children

Getting contents from the children

The next step is to create a context class that will be used to keep track of the contents of the 2 child tag helpers.

public class ModalContext
{
public IHtmlContent Body { get; set; }
public IHtmlContent Footer { get; set; }
}

At the beginning of the ProcessAsync method of the Modal tag helper, create a new instance of ModalContext and add it to the current TagHelperContext:

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{

var modalContext = new ModalContext();
context.Items.Add(typeof(ModalTagHelper), modalContext);
//...
}

Now, in the modal body and modal footer tag helpers we will get the instance of that ModalContext via the TagHelperContext. Instead of rendering the output, these child tag helpers will set the the Body and Footer properties of the ModalContext.

[HtmlTargetElement("modal-body", ParentTag = "modal")]
public class ModalBodyTagHelper : TagHelper
{
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{

var childContent = await output.GetChildContentAsync();
var modalContext = (ModalContext)context.Items[typeof(ModalTagHelper)];
modalContext.Body = childContent;
output.SuppressOutput();
}
}

Back in the modal tag helper, we call output.GetChildContentAsync() which will cause the child tag helpers to execute and set the properties on the ModalContext. After that, we just set the output as we normally would in a tag helper, placing the Body and Footer in the appropriate elements.

Modal tag helper
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var modalContext = new ModalContext();
context.Items.Add(typeof(ModalTagHelper), modalContext);

await output.GetChildContentAsync();

var template =
$@"<div class='modal-dialog' role='document'>
<div class='modal-content'>
<div class='modal-header'>
<button type = 'button' class='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button>
<h4 class='modal-title' id='{context.UniqueId}Label'>{Title}</h4>
</div>
<div class='modal-body'>";

output.TagName = "div";
output.Attributes["role"] = "dialog";
output.Attributes["id"] = Id;
output.Attributes["aria-labelledby"] = $"{context.UniqueId}Label";
output.Attributes["tabindex"] = "-1";
var classNames = "modal fade";
if (output.Attributes.ContainsName("class"))
{
classNames = string.Format("{0} {1}", output.Attributes["class"].Value, classNames);
}
output.Attributes.SetAttribute("class", classNames);
output.Content.AppendHtml(template);
if (modalContext.Body != null)
{
output.Content.AppendHtml(modalContext.Body); //Setting the body contents
}
output.Content.AppendHtml("</div>");
if (modalContext.Footer != null)
{
output.Content.AppendHtml("<div class='modal-footer'>");
output.Content.AppendHtml(modalContext.Footer); //Setting the footer contents
output.Content.AppendHtml("</div>");
}

output.Content.AppendHtml("</div></div>");
}

Conclusion

Composing complex tag helpers with parent / child relationships is fairly straight forward. In my opinion, the approach here is much easier to understand than the “multiple transclusion” approach used to solve the same problem in Angular 1. It would be easy to unit test and as always, Visual Studio provides error messages directly in the HTML editor to guide anyone who is using your tag helper.

You can check out the full source code on the Tag Helper Samples repo.

My Hasty Move to Hexo

As I mentioned in my last post, I had some downtime on my blog after my database mysteriously disappeared.

I have meant for some time now to move my blog to something a little more stable. Wordpress is a fine platform but really overkill for what I need. After moving all my comments to Disqus earlier this year I really had no need at all for a database backend. More importantly, I found it difficult to fine-tune things in Wordpress. Not because it is necessarily difficult to do these things in Wordpress but because have absolutely no interest in learning php.

A Quick Survey

I wanted to move to a statically generated site. I like writing my posts in Markdown and I like the simplicity of a statically generated site. I had a quick look at this site that provides a list of the most popular static site generators.

Jekyll is definitely a great option and seems to be the most popular. At the time, we were using it over at Western Devs. The main problem I have with Jekyll is that it is a bit of a pain to get working on Windows.

I noticed a handy Language filter on the site and picked .NET. There are a few options there but nothing that seems to have any great traction.

Next I picked JavaScript/Node. I am reasonably proficient at JavaScript and I use Node for front-end web dev tasks every day. In that list, Hexo seemed to be the most popular. After polling the group at Western Devs I found out that David Wesst was also using Hexo. This is great for me because Wessty is our resident Node / JavaScript expert. With an expert to fall back on in an emergency situation, I forged ahead in my move to Hexo.

Moving from Wordpress

Hexo provides a plugin for importing from Wordpress. All I did here was followed the steps in the migration documentation. All my posts came across as expected. The only thing that bothered me with the posts is that I lost syntax highlighting on my code blocks. Fixing this was a bit of a manual process, wrapping my code blocks as follows:

{% codeblock lang:html %}
<div>...</div>
{% endcodeblock %}

I did this for my 40 most popular blog posts which covers about 80% of the traffic to my blog. Good enough for me.

Next, I needed to pull down my images. I serve my images on my blog site (I know…I should be hosting this somewhere else like blob storage or imgr). To fix this, I simply used FTP to copy the images from down from my old site and put them in the Hexo source folder. I my case that was the source\wp-content\uploads folder.

Deploying to Azure

I decided to keep my blog hosted in Azure. To deploy to Azure using Hexo, I am using the git deploy method. With this method, anytime I call hexo deploy --generate, Hexo will generate my site and then commit the generated site to a particular branch in my git repository. I then use the Web App continuous deployment hooks in Azure to automatically update the site whenever a change is pushed to that branch.

Some Issues with Hexo

Since I moved my blog over, WesternDevs has also moved to Hexo as part of a big site redesign. Kyle Baley has done a good job of documenting some of the issues we encountered along the way.

I ran into a few more specific issues. First of all, I didn’t want to break all my old links so I kept the same permalinks as my old blog. The challenge with that is that each url ends in .aspx. Weird right…my old blog was Wordpress (php) but before Wordpress I was on geekswithblogs which was using some ASP.NET based blogging engine. So here I am in 2015 with a statically generated blog that is created using Node and is hosted in Azure that for some reason has .aspx file endings. The problem with this was that Aure uses IIS and tries to process the aspx files using the ASP.NET page handlers. Initially everything looked okay. The pages were still being served but some of the characters were not encoded properly. The solution here was to add a web.config to my Hexo source folder. In the web.config I was able to turn off the ASP.NET page handlers and tell IIS that .aspx pages should be treated as static content:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers>
<remove name="PageHandlerFactory-ISAPI-2.0-64" />
<remove name="PageHandlerFactory-ISAPI-2.0" />
<remove name="PageHandlerFactory-Integrated" />
<remove name="PageHandlerFactory-ISAPI-4.0_32bit" />
<remove name="PageHandlerFactory-Integrated-4.0" />
<remove name="PageHandlerFactory-ISAPI-4.0_64bit" />
</handlers>
<staticContent>
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
<mimeMap fileExtension=".aspx" mimeType="text/html" />
<mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
<mimeMap fileExtension=".ttf" mimeType="application/octet-stream" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
</staticContent>
<rewrite>
<rules>
<rule name="RSSRewrite" patternSyntax="ExactMatch">
<match url="feed" />
<action type="Rewrite" url="atom.xml" appendQueryString="false" />
</rule>
<rule name="RssFeedwithslash">
<match url="feed/" />
<action type="Rewrite" url="atom.xml" appendQueryString="false" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

In the web.config I also added a rewrite rule to preserve the old RSS feed link.

Triggering a mass migration

While not perfect, I have been happy with my experience migrating to Hexo. Overall, I was able to complete my initial migration within a few hours. Converting older posts to use syntax highlighting took a litte longer but I was able to do that in phases.

I talked about my experience over at Western Devs and this seems to have triggered a few of us to also move our blogs over to Hexo. Hopefully that decision does come back to bite me later…so far it is working out well.

The Case of the Disappearing Database

Something scary happened last week. The database backing my blog disappeared from my Azure account.

Some background: At the time, my blog was a Wordpress site hosted as an Azure Web Site with a MySQL database hosted by Azure Marketplace provider ClearDB.

Series of events

At approximately 12:01 PM I received an alert from Azure that my blog was returning HTTP 500 errors. I quickly checked the site to see what was happening and I was seeing the dreaded “Error establishing a connection to the database” message. I had seen this in the past because I was hosting on a very small MySQL instance. It was not entirely uncommon to exceed the maximum number of connections to my database. The thing is that I had recently upgraded to a larger database instance specifically to avoid this problem.

So…I logged in to the Azure Portal to investigate. To my horror, the MySql database for my blog was nowhere to be found!!! It was gone from the Azure Portal entirely and I couldn’t find it on the ClearDB website either. I am the only person who has access to this Azure account and I know that I didn’t delete it.

I quickly opened an Azure support ticket and contacted ClearDB to see if either company could tell me what happened to my database.

ClearDB actually responded quickly:

Our records indicate that a remote call from Azure at Wed, 25 Nov 2015 12:00:34 -0600 was issued to us to deprovision the database

Ummm WTF! I know I didn’t delete the database. It seems that there is some kind of bug in the integration between Azure and ClearDB. In the mean time, Azure Support eventually replied with the following:

I have reviewed your case and have adjusted the severity to match the request. Sev A is reserved for situations that involve a system, network, server, or critical program down that severely affects production or profitability. This case is set to severity B with updates every 24 hours or sooner.

After nearly a week, I received another update from Azure support:

I have engaged our Engineering Team already to investigate on this issue and currently waiting for an update from them. Our Engineering Team would require 5 to 7 business days to investigate the issue, I will keep you posted as soon as I hear from them.

I am curious to see what the Engineering Team comes back with. I will update this post if / when I hear more.

Restoring from backup

With my database gone, my only choice was to restore from backup. This should have been an easy task. Unfortunately, my automated backup wasn’t actually running as expected and my most recent backup was 7 months old. I had all my individual posts in Live Writer wpost files but republishing those manually would have taken me over a week.

In the end, ClearDB was very helpful and was able to restore my database from their internal backups. As a result, my blog was down for a little under 24 hours.

Lessons learned

These were hard lessons for me to learn because I already knew these things. Problem was that I wasn’t treating my blog like the production system that it is.

  • Don’t trust the cloud-based backups. ClearDB has automated periodic backups but I lost access to those when my database was mysteriously deleted. Have a backup held offsite. That’s what my Wordpress backups were supposed to do for me, which brings me to my second point.

  • Test your backups periodically. I had no idea my backups weren’t working until it was too late.

  • Complexity kills. I have a simple blog and my comments are managed by Disqus. There is really no reason I should need a relational database for this. The MySQL database here has been a constant source of failure on my blog.

Moving on

Once my blog was restored I quickly started a migration over to Hexo. I will blog more about this process shortly.

Realistic Sample Data with GenFu

Last week, I had the opportunity to spend some time hacking with my good friend James Chambers. One of the projects we worked on is his brainchild: GenFu

GenFu is a test and prototype data generation library for .NET apps. It understands different topics - such as “contact details” or “blog posts” and uses that understanding to populate commonly named properties using reflection and an internal database of values or randomly created data.

As a quick sample, I attempted to replace the Sample Data Generator in the ASP.NET5 Music Store app with GenFu. With the right GenFu configuration, it worked like magic and I was able to remove over 700 lines of code!

As part of that process, it became clear that our documentation related to configuring more complex scenarios was slightly lacking. We are working on creating official project documentation. In the mean time, this post can serve as the unofficial documentation for GenFu.

Installing GenFu

GenFu is available via NuGet and can be added to any .NET 4.5, 4.6, or aspnet5 project.

Install-Package GenFu

Basic Usage

Let’s say you have a simple Contact class as follows:

public class Contact
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAdress { get; set; }
public string PhoneNumber { get; set; }

public override string ToString()
{

return $"{Id}: {FirstName} {LastName} - {EmailAdress} - {PhoneNumber}";
}
}

To generate a list of random people using the GenFu defaults, simply call the A.ListOf method:

var people = A.ListOf<Contact>();
foreach (var person in people)
{
Console.WriteLine(person.ToString());
}

This simple console app will output the following:

That was easy, and the data generally looks pretty realistic. The default is to generate 25 objects. If you want more (or less), you can use an overload of the ListOf method to specify the number of objects you want. Okay, but what if the defaults aren’t exactly what you wanted? That’s where GenFu property filler configuration comes in.

Configuring Property Fillers

GenFu has a fluent API that lets you configure exactly how your object’s properties should be filled.

Manually Overriding Property Fillers

Let’s start with a very simple example. In the example above, the Id is populated with random values. That behaviour might be fine for you, but if you are using GenFu to generate random data to seed a database this will probably cause problems. In this case we would want the Id property to be always set to 0 so the database can automatically generate unique ids.

A.Configure<Contact>()
.Fill(c => c.Id, 0);

var people = A.ListOf<Contact>();

Now all the Ids are 0 and the objects would be safe to save to a database:

Another option is to use a method to fill a property. This can be a delegate or any other method that returns the correct type for the property you are configuring:

var i = 1;

A.Configure<Contact>()
.Fill(c => c.Id, () => { return i++; });

With that simple change, we now have sequential ids. Magic!

There is also an option that allows you to configure a property based on other properties of the object. For example, if you wanted to create an email address that matched the first name/last name you could do the following. Also, notice how you can chain together multiple property configurations.

A.Configure<Contact>()
.Fill(c => c.Id, 0)
.Fill(c => c.EmailAdress,
c => { return string.Format("{0}.{1}@zombo.com", c.FirstName, c.LastName); });\

This can be simplified greatly by using string interpolation in C#6.

A.Configure<Contact>()
.Fill(c => c.Id, 0)
.Fill(c => c.EmailAdress,
c => $"{c.FirstName}.{c.LastName}@zombo.com");

Property Filler Extension Methods

In some cases, you might want to give GenFu hints about how to fill a property. For this there is a set of With* and As* extension methods available. For example, if you wanted an integer property to be filled with values within a particular range:

A.Configure<Contact>()
.Fill(c => c.Age).WithinRange(18, 67);

IntelliSense will show you the list of available extensions based on the type of the property you are configuring.

“IntelliSense showing extensions for a String property”

Extensions are available for String, DateTime, Integer, Short, Decimal, Float and Double types.

WithRandom

In some situations, you might want to fill a property with a random value from a given list of values. A simple example of this might be a boolean value where you want approximately 2/3rds of the values to be true and 1/3 to be false. You could accomplish this using the WithRandom extension as follows:

A.Configure<Contact>()
.Fill(c => c.IsRegistered)
.WithRandom(new bool[] { true, true, false });

The WithRandom method is also useful for wiring up object graphs. Imagine the following model classes:

public class IncidentReport
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime ReportedOn { get; set; }
public Contact ReportedBy { get; set; }
}

public class Contact
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAdress { get; set; }
public string PhoneNumber { get; set; }
}

We could use GenFu to generate 1,000 IncidentReports that were reported by 100 different Contacts as follows:

var contacts = A.ListOf<Contact>(100);

A.Configure<IncidentReport>()
.Fill(r => r.ReportedBy)
.WithRandom(contacts);

var incidentReports = A.ListOf<IncidentReport>(1000);

Wrapping it up

That covers the basics and you are now on your way to becoming a GenFu master. In a future post we will cover how to extend GenFu by writing your own re-usable property fillers. In the mean time, give GenFu a try and let us know what you think.

Markdown in your ASP.NET Core Razor Pages

What? Markdown in your Razor code? Yeah…and it was totally easy to build too.

Taylor Mullen demoed the idea of a Markdown Tag Helper idea at Orchard Harvest and I thought it would be nice to include this in my Tag Helper Samples project.

How to use it

This tag helper allows you to write Markdown directly in Razor and have that automatically converted to HTML at runtime. There are 2 options for how to use this tag helper. The first option is to use a <markdown> element.

<markdown>This is some _simple_ **markdown**.</markdown>

The tag helper will take this and convert it to the following HTML:

<p>This is some <em>simple</em> <strong>markdown</strong>.</p>

The other option is to use a <p> element that has the markdown attribute:

<p markdown>This is some _simple_ **markdown** in a _p_ element.</p>

The tag helper uses MarkdownSharp, which supports most of the markdown syntax supported by Stack Overflow.

How it works

The implementation of this tag helper is surprisingly simple. All we do is grab the contents of the tag and use MarkdownSharp to convert that to HTML.

[HtmlTargetElement("p", Attributes = "markdown")]
[HtmlTargetElement("markdown")]
[OutputElementHint("p")]
public class MarkdownTagHelper : TagHelper
{
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{

if (output.TagName == "markdown")
{
output.TagName = null;
}
output.Attributes.RemoveAll("markdown");

var content = await GetContent(output);
var markdown = content;
var html = CommonMarkConverter.Convert(markdown);
output.Content.SetHtmlContent(html ?? "");
}
}

Try it yourself

You can grab the code from GitHub or install the package using Nuget.

Install-Package TagHelperSamples.Markdown

Give it a try and let me know what you think.