sebnilsson.com | Liquid Development Is What I Do
Seb Nilsson

ASP.NET MVC SEO-Framework

For any serious web-application you should always implement a solid Search engine optimization-strategy, but there is no standardized way to handle this in ASP.NET MVC, out of the box.

You could easily use the ViewBag-object to send dynamic values from Controller-Actions into Views, like this, for example:

public ActionResult Index()
{
    ViewBag.Title = "This is the page title";

    return View();
}

Then you'd have to make sure you correctly spell or copy-paste ViewBag.Title correctly into your View:

<head>
    <title>@ViewBag.Title</title>
    <!-- More head-values -->
</head>

One problem is that if you refactor the naming for ViewBag.Title into, for example ViewBag.Title, this will break the functionality, potentially site-wide, because you won't get any tooling-help from Visual Studio for the renaming.

This is why I created a framework for ASP.NET MVC SEO, to get structured and reusable functionality around the SEO-data for a web-application. The framework is available on Nuget, with the source-code on GitHub.

Using a SeoHelper-object, available inside Controllers and Views, you can set SEO-related data like:

  • Meta-Description
  • Meta-Keywords
  • Title, split on page-title and base-title (website-title)
  • Canonical Link
  • Meta No-index for robots

This can be done inside Controllers and Controller-actions:

[SeoBaseTitle("Website name")]
public class InfoController : SeoController
{
    [SeoTitle("Listing items")]
    [SeoMetaDescription("List of the company's product-items")]
    public ActionResult List()
    {
        var list = GetList();

        if (list.Any())
        {
            Seo.Title += $" (Total: {list.Count})";
            Seo.LinkCanonical = "~/pages/list.html";
        }
        else
        {
            Seo.MetaRobotsNoIndex = true;
        }

        return View(model);
    }
}

Or inside Views:

@{
    Layout = null;
    Seo.MetaRobotsNoIndex = true; // Always block Robots from indexing this View
}

These set values can easily be rendered as HTML-tags inside Views through provided HtmlHelper-extensions:

<head>
    @Html.SeoTitle()

    @Html.SeoLinkCanonical()
    @Html.SeoMetaDescription()
    @Html.SeoMetaKeywords()
    @Html.SeoMetaRobotsIndex()
</head>

See the README-file on GitHub for the latest detailed information about this ASP.NET MVC SEO-framework. Or try it out through Nuget by running Install-Package AspNetMvcSeo in your project. You can even follow the absolutely latest build on MyGet.

Display Local DateTime with Moment.js in ASP.NET

Moment.js

Displaying a DateTime in local format in C# is relatively easy, but it will only use the server's settings to tell what "local" is.

For example, you might want 2016-03-07 14:35 UTC to show as 2016-03-07 15:35 for a visitor from a CET-timezone.

If you want to dynamically show the local date and time you can use the web-client's information through JavaScript and format it with Moment.js, for any user, anywhere in the world.

To do this in a way that is fault-tolerant and also SEO-friendly I want the UTC-DateTime to be hard-coded in the HTML and let Moment.js format it on the fly, when the page loads. To do this I need to populate my .cshtml-file with the following:

<span class="local-datetime"
        title="@(Model.DateUtc.ToString("yyyy-MM-dd HH:mm")) UTC"
        data-utc="@(Model.DateUtc.GetEpochTicks())">
    @(Model.DateUtc.ToString("yyyy-MM-dd HH:mm")) UTC
</span>

Make sure you run .ToUniversalTime() on your DateTime first.

Notice the .GetEpochTicks()-extension method. It makes sure the format of the DateTime is passed in a format that Moment.js can handle easily. The implementation looks like this:

private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

public static double GetEpochTicks(this DateTime dateTime)
{
    return dateTime.Subtract(Epoch).TotalMilliseconds;
}

The last step is to tell Moment.js to format our DateTime to a local format:

$('.local-datetime').each(function() {
    var $this = $(this), utcDate = parseInt($this.attr('data-utc'), 10) || 0;

    if (!utcDate) {
        return;
    }

    var local = moment.utc(utcDate).local();
    var formattedDate = local.format('YYYY-MM-DD HH:mm');
    $this.text(formattedDate);
});

If this (or other, unrelated) JavaScript-code would fail for any reason the UTC-DateTime is the actually HTML-content and will still be displayed.

NullableGuidConstraint for ASP.NET MVC & WebApi

Have you ever written a very usable Route-constraint in ASP.NET MVC or in WebAPI than you wanted to share between them both? For example a constraint that supports nullable Guids (Guid?) as route-parameter.

This can be done by implementing both System.Web.Routing.IRouteConstraint and System.Web.Http.Routing.IHttpRouteConstraint.

Hopefully this article will be obsolete with the release of ASP.NET 5, but until then, here's how you solve this problem:

public class NullableGuidConstraint : IRouteConstraint, IHttpRouteConstraint
{
    // ASP.NET MVC-signature
    public bool Match(
        HttpContextBase httpContext,
        Route route,
        string parameterName,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        return MatchInternal(parameterName, values);
    }

    // WebAPI-signature
    public bool Match(
        HttpRequestMessage request,
        IHttpRoute route,
        string parameterName,
        IDictionary values,
        HttpRouteDirection routeDirection)
    {
        return MatchInternal(parameterName, values);
    }

    private static bool MatchInternal(string parameterName, IDictionary values)
    {
        object value;
        if (!values.TryGetValue(parameterName, out value))
        {
            return false;
        }

        if (value is Guid)
        {
            return true;
        }

        string stringValue = Convert.ToString(value, CultureInfo.InvariantCulture);

        Guid guid;
        bool isMatch = string.IsNullOrWhiteSpace(stringValue) || Guid.TryParse(stringValue, out guid);
        return isMatch;
    }
}

Then you register this constraint through, for example, DefaultInlineConstraintResolver in System.Web.Mvc.Routing for ASP.NET MVC and System.Web.Http.Routing for WebAPI.

var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("guid?", typeof(NullableGuidConstraint));

// ASP.NET MVC
routes.MapMvcAttributeRoutes(constraintResolver);

// WebAPI
routes.MapHttpAttributeRoutes(constraintResolver);

Now you can write attribute-routes like this:

[Route("{controller}/{action}/{id=guid?}")]

PowerShell Profile with Permanent Aliases

Powershell

If you're a PowerShell-user who manually runs scripts or binds an alias every time you open PowerShell, then there is a file you really should know about. It's the PowerShell profile-file (or actually, the six profile-files).

I use the profile-file for Current User in the console located here:

%UserProfile%\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

When you've ensured that this file is created you can enter PowerShell-scripts that will run every time you open your PowerShell-console.

For example you can create a PowerShell-alias for the git-command to work with just using g instead.

set-alias g git

Now I can just type g status and save many, many seconds during a long week of Git-usage.

Render .ascx-Files in ASP.NET MVC Using Only RazorViewEngine

ASP.NET Band-Aid

If you're stuck in an environment where you're migrating from ASP.NET MVC to ASP.NET WebForms it's good to know that you can actually render your existing WebForms-Controls in you MVC-views. This might sound like a crazy thing to do (and it is in the long run!) but it might be useful if you're stuck between sprints and have perfectly working WebForms-Controls (.ascx-files) that you don't have time to migrate right now. All you have to do is use the HtmlHelper's helper-method .RenderPartial(string partialViewName) and pass it the path to the WebForms-Control.

// Write the content of a control inside a view:
Html.RenderPartial("~/Controls/ControlVirtualPath.ascx");

// Or to get the content of a control as a MvcHtmlString, for further manipulation:
Html.Partial("~/Controls/CustomControl.ascx")

It's important that your controls inherits from System.Web.Mvc.ViewUserControl and NOT the old System.Web.UI.UserControl.

One performance-tip that is often mentioned around ASP.NET MVC is to deactivate the WebForms-View Engine for MVC Razor-views (which actually turns out to maybe not make such a big difference after all). This will not prevent .aspx, .ascx and other WebForms-files from working.

But you still want your .ascx-files to work inline in your MVC Razor-views. This can be achieved by implementing your own class that inherits RazorViewEngine, which only uses the WebFormViewEngine when actually needed.

Global.asax (or other config-class)

// Remove WebFormViewEngine (and RazorViewEngine)
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomRazorViewEngine());

CustomRazorViewEngine : System.Web.Mvc.RazorViewEngine

private static readonly WebFormViewEngine WebFormsEngine = new WebFormViewEngine();

public override ViewEngineResult FindPartialView(
    ControllerContext context, string name, bool useCache)
{
    if (name.EndsWith(".ascx"))
    {
        return WebFormsEngine.FindPartialView(context, name, useCache);
    }

    return base.FindPartialView(context, name, useCache);
}

If you need the actual class of the control to do some further analysis/manipulation, you can do the following anywhere in your code:

var viewPage = new ViewPage();
var control = viewPage.LoadControl("~/Controls/ControlVirtualPath.ascx") as ControlType;