受欢迎的博客标签

SubDomain in ASP.NET Core -daniel.jimenez navigating to my-awesome-site.com/daniel.jimenez

Published

Throughout this article, we will build a simple web application to demonstrate some of the changes in ASP.NET Core, like its new request pipeline. This will allow us to explore some of the new features and changes in action, hopefully making them easier to understand. Don’t be concerned, your past experience working with the likes of MVC or Web API is still quite relevant and helpful.

The main purpose of the web application will be allowing public profiles of registered users to be associated with vanity urls. That is, if I select /daniel.jimenez as my vanity url, then visitors will be able to find my profile navigating to my-awesome-site.com/daniel.jimenez .

Note: For those who are new to the concept of a vanity URL, it is a customized short URL, created to brand a website, person, item and can be used in place of traditional longer URLs.

I hope you find this article as interesting to read, as it was for me to write!

Just config a second domain name for your site and use endpoint routing with the RequireHost extension.

ASP.NET Core Vanity URL application - Laying the foundations

Creating a new web application

We could start completely from scratch when creating a new ASP.NET Core application, but for the purposes of this article I will start from one of the templates installed in Visual Studio 2015 as part of the tooling.

Note: If you are using Visual Studio 2015 / Visual Studio Community edition, get VS2015 Update 3 first and then install the .NET Core Tools for Visual Studio.

I have chosen the ASP.NET Core Web Application template including Individual User Accounts as the authentication method.

aspnet-core-project-type

Figure 1. New project type

aspnet-core-template

Figure 2. Starting from a Web Application with Individual User Accounts

This will give us a good starting point for our website. It is also worth mentioning that the authentication will be setup using the new ASP.NET Core Identity framework, including an Entity Framework Core context for storing user accounts.

If you are not using Visual Studio, you should be able to use the yeoman aspnet generators that are part of the OmniSharp project. Their templates are based on the ones included in Visual Studio, so its Web Application template provides a similar starting point.

Registering with a Vanity Url

Initialize the database

Once the new application has been created you should be able to launch it and navigate to /Account/Register, where you will see the initial page for registering new accounts.

create-new-account

Figure 3. Default page for registering new accounts

If you go ahead and try to register, you will find out that your password needs at least one non alphanumeric character, one digit and one upper case letter. You can either match those requirements or change the password options when adding the Identity services in the ConfigureServices method of the Startup class.

Just for the sake of learning, let’s do the latter, and take a first look at the Startup class, where you add and configure the independent modules that our new web application is made of. In this particular case, let’s change the default Identity configuration added by the project template:

services.AddIdentity<ApplicationUser, IdentityRole>(opts => 
{
    opts.Password.RequireNonAlphanumeric = false;
    opts.Password.RequireUppercase = false;
    opts.Password.RequireDigit = false;                    
})

Try again and this time you will see a rather helpful error page that basically reminds you to apply the migrations that initialize the identity schema:

migrations-error

Figure 4.Error prior to applying migrations

Let’s stop the application (or the command will fail) and run the suggested command in a console from the project root folder, the one containing the *.xproj file:

>dotnet ef database update

This will initialize a new database in your SQL Server Local DB instance, including the new Identity schema. If you try to register again, it should succeed and you will have created the first user account.

In Visual Studio, you can quickly open the SQL Server Object Explorer, open the localdb instance, locate the database for your web application, and view the data in the AspNetUsers table:

save-user-localdb

Figure 5. Registered user saved to localdb

Add a VanityUrl column to the schema

So far, so good. The template gave us a good starting point and we have a web application where users can register by creating an account in the database. The next step will be updating the application so users can pick a vanity url when registering.

First we are going to add a new VanityUrl field to the ApplicationUser class, which is the simplest way of adding additional properties to profiles. We will add the property as per the requirement, that is a max length of 256 characters (If you want, you can also go ahead and add additional fields like first name, last name, DoB, etc.):

public class ApplicationUser : IdentityUser
{
    [Required, MaxLength(256)]
    public string VanityUrl { get; set; }
}

Now we need to add a new EF migration (entity framework), so these schema changes get reflected in the database. In the process, we will also add a unique index over the new VanityUrl column. Since we will need to find users given their vanity url, we better speed up those queries!

To add the migration, run the following command from the project root:

>dotnet ef migrations add VanityUrlColumn

This will auto-generate a migration class, but the database won’t be updated until you run the update command. Before that, make sure to update the migration with the unique index over the vanity url field:

public partial class VanityUrlColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name: "VanityUrl",
            table: "AspNetUsers",
            maxLength: 256,
            nullable: false,
            defaultValue: "");
        migrationBuilder.CreateIndex(
            "IX_AspNetUsers_VanityUrl", 
            "AspNetUsers", "VanityUrl", 
            unique: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "VanityUrl",
            table: "AspNetUsers");
        migrationBuilder.DropIndex("IX_AspNetUsers_VanityUrl");
    }
}

Finally go ahead and update the database:

>dotnet ef database update

Update the Register page

Right now our Register page is broken and we are not able to create new user accounts. This makes sense since we added the VanityUrl column as required, but we haven’t updated the register page to capture the new field. We will fix this right now.

Start by adding a new property to the existing RegisterViewModel class. As you might expect, we will add some attributes to make it a required field, allow 3 to 256 characters, and allow only lower case letters, numbers, dashes and dots:

[Required]
[StringLength(256, 
    ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", 
    MinimumLength = 3)]        
[RegularExpression(@"[a-z0-9\.\-]+", 
    ErrorMessage = "Use only lower case letters, numbers, dashes and dots")]
[Display(Name = "Vanity Url")]
public string VanityUrl { get; set; }

Now update the existing view Account\Register.cshtml, adding the VanityUrl field to the form. If you have used MVC before, you might be surprised by the lack of Html helpers. They are still available but, ASP.NET Core has added tag helpers to its tool belt.

With tag helpers, you can write server side rendering code targeting specific html elements (either standard html tags or your own custom tags) that will participate in creating and rendering the final HTML from a razor file. This style of writing your server side views allows for more robust and readable code in your views, seamlessly integrating your server side rendering helpers within the html code.

The razor code for the VanityUrl field will be quite similar to that of the existing fields, using the tag helpers for rendering the label, input and validation message. We will add a bit of flashiness using a bootstrap input group displaying our website’s host, so users can see what their full vanity url would look like:

@ViewContext.HttpContext.Request.Host/
<span asp-validation-for="VanityUrl" class="text-danger"></span> </div> </div>

Finally, update the Register action in the AccountController, so the VanityUrl is mapped from the RegisterViewModel to the ApplicationUser.

var user = new ApplicationUser {
    UserName = model.Email,
    Email = model.Email,
    VanityUrl = model.VanityUrl
};

Users are now able to provide a vanity url while registering, and we will keep that vanity url together with the rest of the user data:

update-register-page

Figure 6. Updated register page

If you added additional profile fields (like first name, last name or DoB) to the ApplicationUser class, follow the same steps with those properties in order to capture and save them to the database.

Adding a public profile page

Right now users can create an account and enter a vanity url like /the-real-foo. The final objective will be associating those urls with a public profile page, but we will start by adding that page. It will initially be accessible only through the standard routing /controller/action/id?, leaving the handling of the vanity urls for the next section.

Create a new ProfileController with a single Details action that receives an id string parameter, which should match the id in the AspNetUsers table. This means using urls like mysite.com/profile/details/b54fb19b-aaf5-4161-9680-7b825fe4f45a, which is rather far from ideal. Our vanity urls as in mysite.com/the-real-foo will provide a much better user experience.

Next, create the Views\Profile\Details.cshtml view and return it from the controller action so you can test if the page is accessible:

public IActionResult Details(string id)
{
    return View();
}

Since we don’t want to expose the ApplicationUser directly in that view (that would expose ids, password hashes etc.), create a new view model named Profile. Add any public properties from ApplicationUser that you want exposed, like the name or DoB. If you didn’t add any extra properties let’s just add the UserName and the VanityuUrl so we have something to show in the page.

You will need to update the Profile\Details.cshtml view so it declares the new Profile class as its model and renders its properties. For the sake of brevity I will skip this, you should be able to manually write your own, or use the Visual Studio wizard for adding a new view, selecting the Details template and our new Profile class as the view model. Please check the source code in GitHub if you find any problems.

A more interesting change is required in the ProfileController, where we need to retrieve an ApplicationUser from the database given its id, and then map it to the new Profile class.

Using dependency injection in the Profile Controller

In order to retrieve an ApplicationUser from the database, the Identity framework already provides a class that can be used for that purpose, the UserManager<ApplicationUser>, which contains a FindByIdAsync method. But how do we access that class from our ProfileController? Here is where dependency injection comes.

Dependency Injection has been built into ASP.NET Core, and it is used by components like the Identity framework to register and resolve their dependencies. Of course, you can also register and resolve your own components. Right now, let’s use constructor injection to receive an instance of the UserManager class in our controller constructor:

private readonly UserManager<ApplicationUser> _userManager;
public ProfileController(UserManager<ApplicationUser> userManager)
{
    _userManager = userManager;
}

If you set a breakpoint, you will see an instance is being provided. This is because in your Startup class, the line services.AddIdentity() has registered that class within the dependency injection container. When an instance of your controller needs to be created, the container realizes a UserManager is needed, providing an instance of the type that was previously registered. (You would get an exception if the required type is not registered)

Now update the action so it finds the user, creates a Profile instance and pass it to the view:

public async Task<IActionResult> Details(string id)
{
    var user = await _userManager.FindByIdAsync(id);
    return View(new Profile 
    { 
        Email = user.Email, 
        Name = user.UserName, 
        VanityUrl = user.VanityUrl 
    });
}

This completes the public profile page, although accessible only with the default routing. We will make sure that page can also be accessed using the vanity urls in the next section!

public-profile-page

Figure 7. Public profile page with default routing

Handling the vanity urls

Middleware to the rescue!

The new request pipeline in ASP.NET Core

The request pipeline in ASP.NET Core is one of the areas with the biggest number of changes. Gone is the request pipeline based on events, and gone are the HttpHandlers and HttpModules of old, that closely followed IIS features.

The new pipeline is leaner, composable and completely independent of the hosting solution. It is based around the concept of middleware, in which a pipeline can be composed of independent modules that receive the request, run its own logic then call the next module. If you have worked with Express in Nodejs, this should sound very familiar!

aspnet-core-request-pipeline

Figure 8. New request pipeline based on middleware

After receiving the response from the next middleware, you also have a chance to run custom logic, potentially updating/inspecting the response. Calling the next module is optional, so some of these modules (like authentication) might decide to end the request earlier than usual.

The ASP.NET Core framework provides a number of built-in middleware like Routing, Authentication or CORS (In case you are wondering, MVC is dependent on the Routing middleware, wired as a route handler). You can also create your own middleware classes or even add them inline as lambda functions.

The place where the middleware components are plugged in to form the request pipeline is the Configure method of the Startup class. As you can imagine, the order in which these are added to the pipeline is critical! The Startup class of this project contains by default:

app.UseStaticFiles();
app.UseIdentity();
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Adding custom middleware for the vanity urls

Ok, so we can add our own middleware to the pipeline. How will that help us with the vanity urls?

The idea is to create a new piece of middleware that inspects the url before the routing middleware. If the url has a single segment, it could be one of our vanity urls, so we will query the database for a user account with that vanity url. In case we have a match, we are going to update the path in the Request object so it matches the standard route for the public profile page. This process will happen server side, without client redirections involved.

You might be wondering why are we using a middleware for this, and not the routing features? In ASP.NET Core, middleware components are the proper place to inspect and modify the request and/or response objects. This allows writing very simple and testable components following the single responsibility principle. These components can then be composed in several different ways in order to build the request processing pipeline. In our case:

  • The new middleware component resolves vanity urls into a standard controller/action/id url.
  • The routing middleware continues performing url pattern matching to find the right controller and action.

In summary, when we process a url like mysite.com/the-real-foo, our middleware component will find the foo user in the database, get its ApplicationUser object which includes its id, and then update the url in the request object to be mysite.com/profile/details/b54fb19b-aaf5-4161-9680-7b825fe4f45a. After that we will call next(); to execute the next middleware in the pipeline, which means the routing middleware will be able to send the request to our ProfileController!

request-pipeline

Figure 9. Request pipeline including the VanityUrl middleware

Let’s go ahead and create a new class VanityUrlsMiddleware inside a new Middleware folder (notice how you can use dependency injection again to receive the UserManager):

public class VanityUrlsMiddleware
{
    private readonly RequestDelegate _next;
    private readonly UserManager<ApplicationUser> _userManager;        
    public VanityUrlsMiddleware(
        RequestDelegate next, 
        UserManager<ApplicationUser> userManager)
    {
        _next = next;
        _userManager = userManager;
    }

    public async Task Invoke(HttpContext context)
    {
        await HandleVanityUrl(context);
        //Let the next middleware (MVC routing) handle the request
        //In case the path was updated,
        //the MVC routing will see the updated path
        await _next.Invoke(context);
    }
    private async Task HandleVanityUrl(HttpContext context)
    {
        //TODO
    }

Now add it to the pipeline right before adding MVC in the Configure method of the Startup class:

app.UseMiddleware<VanityUrlsMiddleware>();
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

If you run the application now, you should be able to set a breakpoint inside your middleware class, and it will be hit on every request.

Let’s finish the middleware by adding the following logic to the HandleVanityUrl method:

1. Make sure the request path has a single segment and matches the regex for vanity urls. Remember we added a regex on the RegisterViewModel to allow only lower case letters, numbers, dashes and dots? We can use the same regex here, extracting it as a constant.

2. Query the database, trying to find a user with that vanity url

3. Replace the Path property of the Request object with the pattern profile/details/{id}

This code will look like the following:

private async Task HandleVanityUrl(HttpContext context)
{
    //get path from request
    var path = context.Request.Path.ToUriComponent();
    if (path[0] == '/')
    {
        path = path.Substring(1);
    }

    //Check if it matches the VanityUrl regex
    //(single segment, only lower case letters, dots and dashes)
    //Check accompanying sample project for more details
    if (!IsVanityUrl(path))
    {
        return;
    }

    //Check if a user with this vanity url can be found
    var user = await _userManager.Users.SingleOrDefaultAsync(u => 
        u.VanityUrl.Equals(path, 
                      StringComparison.CurrentCultureIgnoreCase));
    if (user == null)
    {
        return;
    }

    //If we got this far, the url matches a vanity url,
    //which can be resolved to the profile details page.
    context.Request.Path = String.Format(
                       "/profile/details/{0}", user.Id);
}

That’s it, now you have vanity urls working in your application! If you inspect the network tab of your browser, you will see there were no redirects, it all happened seamlessly on the server:

vanilty-url-requests

Figure 11. Requests when accessing a vanity url

Validate user selected urls

You might remember that when we added the VanityUrl column to the database, we created a Unique Index. This means an exception is raised if you try to register a new user with a vanity url already in use.

This isn’t the world’s best user experience, although it might be enough for this article. However the Remote attribute is still available which means we can quickly improve our application!

Note: If you are not familiar with previous versions of ASP MVC, the Remote attribute is one of the built in validation attributes that adds client and/or server side validations. This particular attribute adds a client side validation which will call the specified controller action.

We just need to add a new controller action that returns a JSON indicating whether the value matches an existing vanity url or not. I decided to add this action within the ProfileController so the validation logic stays close to its use case, but the AccountController will be an equally valid option:

public async Task<JsonResult> ValidateVanityUrl(string vanityUrl)
{
    var user = await _userManager.Users.SingleOrDefaultAsync(
              u => u.VanityUrl == vanityUrl);
    return Json(user == null);
}

Then add the Remote attribute to the property in the RegisterViewModel:

..
[Remote("ValidateVanityUrl", "Profile", 
        ErrorMessage = "This vanity url is already in use")]
…
public string VanityUrl { get; set; }

Wrapping up

Improvements in our application

Since we have already covered a lot of ground, I have left out a few improvements that can be easily applied to this code. Feel free to get the source from GitHub and play with it:

  • Every time a vanity url is resolved, the ApplicationUser is retrieved twice from the database. This can be improved, for example by simply saving the ApplicationUser to the HttpContext.Items, and updating the ProfileController to first check that collection before hitting the database again. Furthermore, this can be moved to a ProfileService injected into the controller.
  • The middleware has the resolved url pattern profile/details/{0} hardcoded internally. This is a good excuse to explore the Configuration and Options libraries built into ASP.Net Core!
  • The logic to find an ApplicationUser given its vanity url is duplicated in two different places. This is a good candidate to be extracted into a UserService, injected in those places.

Conclusion

We have just seen the tip of the iceberg in terms of what ASP.NET Core has to offer. It contains fundamental changes like its new request pipeline or built-in dependency injection, and improvements not that disruptive as the tag helpers.

However ASP.NET Core should be very familiar and easy to get started for developers used to the old ASP.Net. You should be able to start working with a leaner, cross-platform, composable framework without your current knowledge becoming completely obsolete. In fact, you should be able to transfer most of that knowledge.

For further reading, I would recommend checking the new official documentation site and the framework GitHub page:

The source code of this article is available on GitHub as well:

https://github.com/DaniJG/AspCoreVanityUrls