Building an ASP.NET Core Starter App on MacOS (Automated Testing)

In the previous post, we completed our initial configuration of the Identity configuration, and added basic authentication and authorization to the example application.  Like our previous posts, source code (with better formatting!) for this post can be found at https://github.com/jrodenbostel/example-web-application.  There will be revision for all of the changes mentioned in this post.  In this post, we’ll cover adding automated tests to our application.  This topic is where I personally had the most difficult time finding examples – as mentioned before, the struggles were specifically related to mocking Identity related services and getting integration tests up and running on secured methods and against the AccountController.

To start, we need to add an automated test project to our solution.  In order to do that, I had to move the location of the project root and rearrange the repository (and solution) so that it included a root solution with two sub-projects: one for the application, and one for tests. The first revision you’ll see in the repository is the result of the projects being rearranged into a solution and the test project being added.  I used a project template to create the new project. You’ll see it is named “ExampleWebApplicationTest” and is packaged with the xUnit library unit testing and Moq library for creating mock objects.

After creating the test project, I created a reference to ExampleWebApplication from ExampleWebApplicationTest so the test project can use the libraries included in the ExampleWebApplication project.

Our goal in this post is to create a suite of unit and integration tests that exercise the logic in our ExampleWebApplication project.  Since our integration tests will leverage an in-memory database and will need to “start up” like a normal application, we need to update the test project’s project configuration file to use the web sdk.

/ExampleWebApplicationTest/ExampleWebApplicationTest.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ExampleWebApplication\ExampleWebApplication.csproj" />
</ItemGroup>
</Project>

After that, we’ll get started by installing some new packages.  One library of note in the following list is “MockQueryable”, which is a set of helper classes for creating mocks of IQueryable collections (https://github.com/romantitov/MockQueryable).  Very handy.  From the command line:

dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Moq
dotnet add package MockQueryable.Moq
dotnet add package Microsoft.AspNet.WebApi.Client

Next, let’s add a “Controllers” directory and create our first unit test file for the AccountController.  This file has TONS of examples and workarounds.  One of the interesting additions is the AccountControllerWrappers method, which wraps difficult/impossible to mock methods so that they can be mocked.  Note that the code this wrapper uses is largely Microsoft framework code that I don’t feel is worth testing. When you ask yourself how to mock normal controller interactions, Identity-specific services (such as SignInManager and UserManager), or EntityFramework-related classes, this is the place to look – all of these were things I struggled with and this test class contains examples of working with each.

ExampleWebApplicationTest/Controllers/AccountControllerTest.cs

using System;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using ExampleWebApplication.Models;
using ExampleWebApplication.Models.ViewModels;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace ExampleWebApplication.Controllers
{
public class AccountController : Controller
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly ILogger _logger;
private readonly IEmailSender _emailSender;
private readonly IConfiguration _configuration;
public IAccountControllerWrappers Wrappers { get; set; }
public AccountController(
UserManager<User> userManager,
SignInManager<User> signInManager,
ILogger<AccountController> logger,
IEmailSender emailSender,
IConfiguration configuration)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
_configuration = configuration;
Wrappers = new AccountControllerWrappers();
}
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
if (!Convert.ToBoolean(_configuration["EnableRegistration"]))
{
// If we got this far, something failed, redisplay form
ViewData["Error"] = "Registration disabled!";
return View();
}
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
var user = new User {UserName = model.Email, Email = model.Email};
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Wrappers.GetActionLink(Url, nameof(ConfirmEmail), user, code);
await _emailSender.SendEmailAsync(model.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
TempData["Information"] = "Confirmation email sent.";
_logger.LogInformation("User created a new account with password.");
return RedirectToLocal(returnUrl);
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ConfirmEmail(string userId, string code)
{
var user = await _userManager.FindByIdAsync(userId);
var result = await _userManager.ConfirmEmailAsync(user, code);
if (result.Succeeded)
{
TempData["Information"] = "Registration confirmed!";
return RedirectToAction(nameof(Login));
}
TempData["Error"] = "Something went wrong.";
return RedirectToAction(nameof(Register));
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnUrl = null)
{
// Clear the existing external cookie to ensure a clean login process
await Wrappers.SignOutAsync(HttpContext);
ViewData["ReturnUrl"] = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe,
lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToAction(nameof(Lockout));
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
_logger.LogInformation("User logged out.");
return RedirectToAction(nameof(HomeController.Index), "Home");
}
[HttpGet]
[AllowAnonymous]
public IActionResult Lockout()
{
return View();
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private IActionResult RedirectToLocal(string returnUrl)
{
if (returnUrl != null && Wrappers.IsLocalUrl(Url, returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction(nameof(HomeController.Index), "Home");
}
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
{
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Wrappers.GetActionLink(Url, nameof(ResetPassword), user, code);
await _emailSender.SendEmailAsync(model.Email, "Reset your password",
$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
TempData["Information"] = "Password reset email sent.";
return RedirectToAction(nameof(Login));
}
TempData["Error"] = "No user with that email address found.";
return RedirectToAction(nameof(Register));
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ResetPassword(string userId, string code)
{
var user = await _userManager.FindByIdAsync(userId);
if (user != null)
{
var viewModel = new ResetPasswordViewModel {Email = user.Email, Code = code};
return View(viewModel);
}
TempData["Error"] = "Invalid confirmation code.";
return RedirectToAction(nameof(Register));
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user != null)
{
await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
await _emailSender.SendEmailAsync(model.Email, "Password reset notification",
$"Your password has been reset successfully.");
TempData["Information"] = "Password reset successfully.";
return RedirectToAction(nameof(Login));
}
TempData["Error"] = "No user with that email address found.";
return RedirectToAction(nameof(Register));
}
public IActionResult ExternalLogin()
{
throw new NotImplementedException();
}
}
public class AccountControllerWrappers : IAccountControllerWrappers
{
public async Task SignOutAsync(HttpContext context)
{
await context.SignOutAsync(IdentityConstants.ExternalScheme);
}
public string GetActionLink(IUrlHelper helper, string action, User user, string code)
{
return helper.ActionLink(action,
values: new {userId = user.Id, code = code});
}
public bool IsLocalUrl(IUrlHelper helper, string url)
{
return helper.IsLocalUrl(url);
}
}
public interface IAccountControllerWrappers
{
public Task SignOutAsync(HttpContext context);
public string GetActionLink(IUrlHelper helper, string action, User user, string code);
public bool IsLocalUrl(IUrlHelper helper, string url);
}
}

Setting up unit testing is the easy part! Configuring our test project to also execute end-to-end integration tests involves a little more work. Let’s start by creating a test version of our Startup file that extends the Startup configuration from the ExampleWebApplication project.  You should notice here that we are swapping out our database configuration for one that uses the in-memory SQLite implementation.  This is useful for keeping our automated tests away from altering data in any of our “live” databases and prevents a build-time dependency on database availability.

ExampleWebApplicationTest/TestStartup.cs

using ExampleWebApplication;
using ExampleWebApplication.Data;
using ExampleWebApplication.Models;
using ExampleWebApplication.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace ExampleWebApplicationTest
{
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration) : base(configuration)
{
}
public override void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
const string connectionString = "DataSource=:memory:";
var connection = new SqliteConnection(connectionString);
connection.Open();
services.AddDbContext<DefaultContext>(options =>
options.UseSqlite(connection));
services.AddDbContext<IdentityContext>(options =>
options.UseSqlite(connection));
services.AddIdentity<User, IdentityRole>(config => { config.SignIn.RequireConfirmedEmail = false; })
.AddEntityFrameworkStores<IdentityContext>()
.AddDefaultTokenProviders();
services.AddTransient<IEmailSender, MockEmailSender>();
services.AddAuthorization();
}
}
public class MockEmailSender : EmailSender
{
}
}

When integration testing, I prefer to continually reset the database to a known state before my tests run.  I created a test version of the SeedData process to accomplish this, and you’ll notice calls in the classes that follow that run our migration process before seeding our database.

using System;
using System.Linq;
using ExampleWebApplication.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace ExampleWebApplicationTest
{
public static class TestSeedData
{
public const string TestEmail = "justin@rodenbostel.com";
public const string TestPassword = "Test_password1234";
public static void Initialize(IServiceProvider serviceProvider)
{
SeedRoles(serviceProvider);
SeedUsers(serviceProvider);
}
private static void SeedUsers(IServiceProvider serviceProvider)
{
var userManager = serviceProvider.GetRequiredService<UserManager<User>>();
foreach (var user in userManager.Users.ToList())
{
userManager.DeleteAsync(user).Wait();
}
if (userManager.FindByEmailAsync(TestEmail).Result == null)
{
var user = new User {UserName = TestEmail, Email = TestEmail, EmailConfirmed = true};
var result = userManager.CreateAsync(user, TestPassword).Result;
if (result.Succeeded)
{
userManager.AddToRoleAsync(user, "Admin").Wait();
}
}
}
private static void SeedRoles(IServiceProvider serviceProvider)
{
var roleManager
= serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
foreach (var role in roleManager.Roles.ToList())
{
roleManager.DeleteAsync(role).Wait();
}
if (!roleManager.RoleExistsAsync("Admin").Result)
{
var role = new IdentityRole {Name = "Admin"};
var roleResult = roleManager.CreateAsync(role).Result;
}
}
}
}

Now let’s create a WebApplicationFactory that we can use to run our app during the tests:

ExampleWebApplicationTest/TestWebApplicationFactory.cs

using System;
using ExampleWebApplication.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace ExampleWebApplicationTest
{
public class TestWebApplicationFactory<TStartup>
: WebApplicationFactory<TestStartup>
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder().ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<TestStartup>().UseTestServer();
});
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Build the service provider.
var serviceProvider = services.BuildServiceProvider();
// Create a scope to obtain a reference to the database
// context (ApplicationDbContext).
using (var scope = serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var identityDb = scopedServices.GetRequiredService<IdentityContext>();
var logger = scopedServices
.GetRequiredService<ILogger<TestWebApplicationFactory<TestStartup>>>();
// Ensure the database is created.
identityDb.Database.Migrate();
identityDb.Database.EnsureCreated();
try
{
// Seed the database with test data.
TestSeedData.Initialize(serviceProvider);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding the " +
"database with test messages. Error: {Message}", ex.Message);
}
}
});
}
}
}

At this point, the plumbing for integration testing should be in place.  Next we’ll create a helper class for integration test development to house/run our boilerplate set up/tear down logic.  One of the big concerns in this file is dealing with spoofing CSRF tokens for forgery-protected controller methods.  I created a directory called “Integration” for this file and it’s implementations.

ExampleWebApplicationTest/Integration/IntegrationTest.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace ExampleWebApplicationTest.Integration
{
public abstract class IntegrationTest : IClassFixture<TestWebApplicationFactory<TestStartup>>
{
private SetCookieHeaderValue _antiForgeryCookie;
private string _antiForgeryToken;
private static readonly Regex AntiForgeryFormFieldRegex =
new Regex(@"\<input name=""__RequestVerificationToken"" type=""hidden"" value=""([^""]+)"" \/\>");
private async Task<string> EnsureAntiForgeryToken(HttpClient client)
{
if (_antiForgeryToken != null) return _antiForgeryToken;
var response = await client.GetAsync("/Account/Login");
response.EnsureSuccessStatusCode();
if (response.Headers.TryGetValues("Set-Cookie", out IEnumerable<string> values))
{
_antiForgeryCookie = SetCookieHeaderValue.ParseList(values.ToList()).SingleOrDefault(c =>
c.Name.StartsWith(".AspNetCore.AntiForgery.", StringComparison.InvariantCultureIgnoreCase));
}
Assert.NotNull(_antiForgeryCookie);
client.DefaultRequestHeaders.Add("Cookie",
new CookieHeaderValue(_antiForgeryCookie.Name, _antiForgeryCookie.Value).ToString());
var responseHtml = await response.Content.ReadAsStringAsync();
var match = AntiForgeryFormFieldRegex.Match(responseHtml);
_antiForgeryToken = match.Success ? match.Groups[1].Captures[0].Value : null;
Assert.NotNull(_antiForgeryToken);
return _antiForgeryToken;
}
private async Task<Dictionary<string, string>> EnsureAntiForgeryTokenForm(HttpClient client,
Dictionary<string, string> formData = null)
{
if (formData == null) formData = new Dictionary<string, string>();
formData.Add("__RequestVerificationToken", await EnsureAntiForgeryToken(client));
return formData;
}
protected async Task PerformLogin(HttpClient client, string username, string password)
{
var formData = await EnsureAntiForgeryTokenForm(client, new Dictionary<string, string>
{
{"Email", username},
{"Password", password}
});
var response = await client.PostAsync("/Account/Login", new FormUrlEncodedContent(formData));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// The current pair of anti-forgery cookie-token is not valid anymore
// Since the tokens are generated based on the authenticated user!
// We need a new token after authentication (The cookie can stay the same)
_antiForgeryToken = null;
}
}
}

And last, we’ll create our integration test file, which contains methods that exercise the application from controller methods through to our configured in-memory database.

ExampleWebApplication/Integration/AccountIntegrationTest.cs

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace ExampleWebApplicationTest.Integration
{
public sealed class BasicIntegrationTest : IntegrationTest
{
private readonly TestWebApplicationFactory<TestStartup> _factory;
public BasicIntegrationTest(TestWebApplicationFactory<TestStartup> factory)
{
_factory = factory;
}
[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
// Arrange
var client = GetClient(false);
// Act
var response = await client.GetAsync("/");
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Contains("Account/Login",
response.Headers.Location.OriginalString);
}
[Fact]
public async Task ShouldLoginAndRenderHome()
{
//Arrange
var client = GetClient();
await PerformLogin(client, TestSeedData.TestEmail, TestSeedData.TestPassword);
//Act
var response = await client.GetAsync("/");
var body = response.Content.ReadAsStringAsync().Result;
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("Home", body);
}
private HttpClient GetClient(Boolean redirects = true)
{
return _factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = redirects
});
}
}
}

To test this is all functioning properly, run `dotnet test` from the root of your solution.  We should see 18 passing testing and a decent amount of coverage on our AccountController.

This is the final post. As I previously mentioned, inspiration for these posts and this code came from a variety of sources. By no means could any of this been accomplished without blogs, StackOverflow, Microsoft documentation, and many other sources.  I hope this series of posts helps those struggling in the same way I was create what I think is a more full-featured starting point for ASP.NET Core MVC development.

Building an ASP.NET Core Starter App on MacOS (Authentication & Authorization)

In the previous post, we covered moving away from the default front-end tools and adding support for dependencies managed by NPM and static asset assembly with Webpack.  We also updated our layout to utilize some updated styles and a Vue component.  In this post, we’ll cover adding authentication and authorization using a database source.  Again, source code for this post (and all subsequent posts in this series) are available at: https://github.com/jrodenbostel/example-web-application.

There is a comprehensive tutorial on getting up and running with basic authentication/authorization available from Microsoft (https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-3.1&tabs=netcore-cli).  If getting up and running quickly while getting an understanding of how the tools work is your goal, this is the tutorial for you.  I found that it fell short in several areas – enough to the point of searching high and low for information how to create the default configuration from scratch, but in a way that is easily customizable – both styles and functionality.  Here are some specifics:

  • All of the identity stuff is built into a Razor Class Library – so unless you’re digging through the source of these libraries, it’s really difficult to understand exactly what is going on, and even more difficult to customize from a functionality perspective
  • When “override” views are provided, they seem to ignore custom layouts and continue to use jQuery and Bootstrap. I was able to code around this problem on my local machine, but not when my app was deployed to Azure App Service. When deployed to Azure, my override views ignored my layout.
  • There are many identity-related classes (UserManager, SignInManager) that are difficult to unit test, especially when creating mocks. So if you are able to customize the functionality, exercising your customization with unit tests can be a struggle (we’ll cover this topic in the next post).

I believe this situation is partially frustrating because we’re expected to just “right-click and generate” without being trusted with the source code the way we are for other scaffolded items.

I found a great tutorial here: https://www.tektutorialshub.com/asp-net-core/asp-net-core-identity-tutorial/#configuring-the-identity-services. This is largely the inspiration for my design and in many places, an exact copy.  Thank you to whomever wrote that.  Amazing.

In the rest of this tutorial, I’ll quickly walk through dependencies to add, database migrations to generate, and models/views/controllers to create.  At the end, we’ll seed some test data and try it all out.  Please note these examples continue to use Bulma and Vue in place of jQuery and Bootstrap.

If you’re using the source code from Github, you’ll notice our app has no models at this point, but that I’ll be continuing down the SQL Server path given what I wrote in the first post.  The examples below assume you have a ConnectionString in your configuration file name ‘DefaultConnection’.

To get started, we’ll have to add some packages to our solution. These should be all that are needed for now. From the command line:


dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design

dotnet add package Microsoft.EntityFrameworkCore.Design

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

dotnet add package Microsoft.AspNetCore.Identity.UI

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

dotnet add package Microsoft.EntityFrameworkCore.Tools

Next we’ll add a couple of models to our app.  One will serve as our User, where Identity information is stored.  The other is a sample example.

/Models/TestModel.cs


using System;

using System.ComponentModel.DataAnnotations;

namespace ExampleWebApplication.Models

{

public class TestModel

{

public int Id { get; set; }

[Required]

public string Description { get; set; }

}

}

/Models/User.cs


using Microsoft.AspNetCore.Identity;

namespace ExampleWebApplication.Models

{

public class User : IdentityUser

{

}

}

The next step is wiring our models into contexts so that we can generate and run migrations and begin storing data.  I prefer to keep my Identity-related models in a seperate context.  I have found that it makes managing migrations and using SeedData during integration testing much easier. I am storing my Contexts in a folder named ‘Data’.

/Data/DefaultContext.cs


using ExampleWebApplication.Models;

using Microsoft.EntityFrameworkCore;

namespace ExampleWebApplication.Data

{

public class DefaultContext : DbContext

{

public DefaultContext(DbContextOptions<DefaultContext> options)

: base(options)

{

}

public DbSet<TestModel> TestModels { get; set; }

}

}

/Data/IdentityContext.cs


using ExampleWebApplication.Models;

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

using Microsoft.EntityFrameworkCore;

namespace ExampleWebApplication.Data

{

public class IdentityContext : IdentityDbContext<User>

{

public IdentityContext(DbContextOptions<IdentityContext> options)

: base(options)

{

}

}

}

This will come up later, but ‘a nice to have’ part of the registration process is the ability to confirm user email addresses with confirmation emails.  This also helps in password reset situations.  For the time being we’ll create and register an empty email service, and we’ll store it in the “/Services” folder.

/Services/EmailSender.cs


using System.Threading.Tasks;

using Microsoft.AspNetCore.Identity.UI.Services;

namespace ExampleWebApplication.Services

{

public class EmailSender : IEmailSender

{

public Task SendEmailAsync(string email, string subject, string htmlMessage)

{

throw new System.NotImplementedException();

}

}

}

We have the foundation set up at this point, but now we need to wire everything into our Startup configuration.  You’ll notice that we’ve disabled email confirmation for the time being.

/Startup.cs


using ExampleWebApplication.Data;

using ExampleWebApplication.Models;

using ExampleWebApplication.Services;

using Microsoft.AspNetCore.Builder;

using Microsoft.AspNetCore.Hosting;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Identity.UI.Services;

using Microsoft.EntityFrameworkCore;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Microsoft.Extensions.Hosting;

namespace ExampleWebApplication

{

public class Startup

{

public Startup(IConfiguration configuration)

{

Configuration = configuration;

}

public IConfiguration Configuration { get; }

 

public void ConfigureServices(IServiceCollection services)

{

services.AddControllersWithViews();

 

services.AddDbContext<DefaultContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddDbContext<IdentityContext>(options =>

options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

services.AddIdentity<User, IdentityRole>(config => { config.SignIn.RequireConfirmedEmail = false; })

.AddEntityFrameworkStores<IdentityContext>()

.AddDefaultTokenProviders();

services.AddTransient<IEmailSender, EmailSender>();

services.AddSingleton<IConfiguration>(Configuration);

 

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

if (env.IsDevelopment())

{

app.UseDeveloperExceptionPage();

}

else

{

app.UseExceptionHandler("/Home/Error");

app.UseHsts();

}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints =>

{

endpoints.MapControllerRoute(

name: "default",

pattern: "{controller=Home}/{action=Index}/{id?}");

});

}

}

}

Use command line tools to generate our migrations and then use those migrations to generate our database tables.  Earlier I mentioned having seperate database contexts, and since our intent is to keep the Identity configuration seperate from the rest of our data config, we’ll generate our non-identity migrations first. From the command line:


dotnet ef migrations add CreateDefaultSchema --context DefaultContext

dotnet ef migrations add CreateDefaultSchema --context IdentityContext

dotnet ef database update --context DefaultContext

dotnet ef database update --context IdentityContext

We can test the health of our application by starting it up.  You should not see any errors in your server logs, and the app should render as it did at the end of the previous post.

Some of the controllers we’ll code use ViewModels.  Before we write our views, let’s create a few.  I created a subfolder in /Models called “ViewModels”.

/Models/ViewModels/LoginViewModel.cs


using System.ComponentModel.DataAnnotations;

namespace ExampleWebApplication.Models.ViewModels

{

public class LoginViewModel

{

[Required] [EmailAddress] public string Email { get; set; }

[Required]

[DataType(DataType.Password)]

public string Password { get; set; }

[Display(Name = "Remember me?")] public bool RememberMe { get; set; }

}

}

/Models/ViewModels/RegisterViewModel.cs


using System.ComponentModel.DataAnnotations;

namespace ExampleWebApplication.Models.ViewModels

{

public class RegisterViewModel

{

[Required]

[EmailAddress]

[Display(Name = "Email")]

public string Email { get; set; }

[Required]

[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",

MinimumLength = 6)]

[DataType(DataType.Password)]

[Display(Name = "Password")]

public string Password { get; set; }

[DataType(DataType.Password)]

[Display(Name = "Confirm password")]

[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]

public string ConfirmPassword { get; set; }

}

}

/Models/ViewModels/ResetPasswordViewModel.cs


using System.ComponentModel.DataAnnotations;

namespace ExampleWebApplication.Models.ViewModels

{

public class ResetPasswordViewModel

{

[Required] public string Code { get; set; }

[Required]

[EmailAddress]

[Display(Name = "Email")]

public string Email { get; set; }

[Required]

[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.",

MinimumLength = 6)]

[DataType(DataType.Password)]

[Display(Name = "Password")]

public string Password { get; set; }

[DataType(DataType.Password)]

[Display(Name = "Confirm password")]

[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]

public string ConfirmPassword { get; set; }

}

}

/Models/ViewModels/ForgotPasswordViewModel.cs


using System.ComponentModel.DataAnnotations;

namespace ExampleWebApplication.Models.ViewModels

{

public class ForgotPasswordViewModel

{

[Required] [EmailAddress] public string Email { get; set; }

}

}

Next, we’ll start creating views for the various parts of our security configuration.  Following someone else’s example, I chose to store my Identity-related views in a folder named “Account”.

/Views/Account/Login.cshtml


@using Microsoft.AspNetCore.Identity

@model ExampleWebApplication.Models.ViewModels.LoginViewModel

@inject SignInManager<User> SignInManager

@{

ViewData["Title"] = "Log in";

Layout = "_Layout";

}

<div class="container">

<div class="column is-half">

<section>

<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">

<h4 class="title is-4">Use a local account to log in.</h4>

<hr/>

<div asp-validation-summary="All" class="has-text-danger"></div>

<div class="field">

<label class="label" asp-for="Email"></label>

<div class="control">

<input asp-for="Email" class="input">

</div>

<span asp-validation-for="Email" class="help is-danger"></span>

</div>

<div class="field">

<label class="label" asp-for="Password"></label>

<div class="control">

<input asp-for="Password" class="input">

</div>

<span asp-validation-for="Password" class="help is-danger"></span>

</div>

<div class="field">

<label class="checkbox" asp-for="RememberMe">

<input asp-for="RememberMe">

@Html.DisplayNameFor(m => m.RememberMe)

</label>

</div>

<div class="field is-grouped">

<div class="control">

<button class="button is-link">Login</button>

</div>

</div>

<p>

<a asp-action="ForgotPassword">Forgot your password?</a>

</p>

<p>

<a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">Register as a new user?</a>

</p>

</form>

</section>

</div>

</div>

/Views/Account/Register.cshtml


@model ExampleWebApplication.Models.ViewModels.RegisterViewModel

@{

ViewBag.Title = "Register";

Layout = "_Layout";

}

<div class="container">

<div class="column is-half">

<section>

<form asp-route-returnUrl="@ViewData["ReturnUrl"]" method="post">

<h4 class="title is-4">Create a new account.</h4>

<hr/>

<div asp-validation-summary="All" class="has-text-danger"></div>

<div class="field">

<label class="label" asp-for="Email"></label>

<div class="control">

<input asp-for="Email" class="input">

</div>

<span asp-validation-for="Email" class="help is-danger"></span>

</div>

<div class="field">

<label class="label" asp-for="Password"></label>

<div class="control">

<input asp-for="Password" class="input">

</div>

<span asp-validation-for="Password" class="help is-danger"></span>

</div>

<div class="field">

<label class="label" asp-for="Password"></label>

<div class="control">

<input asp-for="ConfirmPassword" class="input">

</div>

<span asp-validation-for="ConfirmPassword" class="help is-danger"></span>

</div>

<div class="field is-grouped">

<div class="control">

<button class="button is-link">Register</button>

</div>

</div>

</form>

</section>

</div>

</div>

/Views/Account/ResetPassword.cshtml


@model ExampleWebApplication.Models.ViewModels.ResetPasswordViewModel

@{

ViewBag.Title = "Reset Password";

Layout = "_Layout";

}

<div class="container">

<div class="column is-half">

<section>

<form asp-route-returnUrl="@ViewData["ReturnUrl"]" method="post">

<input type="hidden" asp-for="Code"/>

<h4 class="title is-4">Reset your password.</h4>

<hr/>

<div asp-validation-summary="All" class="has-text-danger"></div>

<div class="field">

<label class="label" asp-for="Email"></label>

<div class="control">

<input asp-for="Email" class="input">

</div>

<span asp-validation-for="Email" class="help is-danger"></span>

</div>

<div class="field">

<label class="label" asp-for="Password"></label>

<div class="control">

<input asp-for="Password" class="input">

</div>

<span asp-validation-for="Password" class="help is-danger"></span>

</div>

<div class="field">

<label class="label" asp-for="Password"></label>

<div class="control">

<input asp-for="ConfirmPassword" class="input">

</div>

<span asp-validation-for="ConfirmPassword" class="help is-danger"></span>

</div>

<div class="field is-grouped">

<div class="control">

<button class="button is-link">Reset Password</button>

</div>

</div>

</form>

</section>

</div>

</div>

/Views/Account/ForgotPassword.cshtml


@model ExampleWebApplication.Models.ViewModels.LoginViewModel

@{

ViewData["Title"] = "Forgot Password";

Layout = "_Layout";

}

<div class="container">

<div class="column is-half">

<section>

<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">

<h4 class="title is-4">Enter your account's email address.</h4>

<hr/>

<div asp-validation-summary="All" class="has-text-danger"></div>

<div class="field">

<label class="label" asp-for="Email"></label>

<div class="control">

<input asp-for="Email" class="input">

</div>

<span asp-validation-for="Email" class="help is-danger"></span>

</div>

<div class="field is-grouped">

<div class="control">

<button class="button is-link">Reset Password</button>

</div>

</div>

<p>

<a asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">Register as a new user?</a>

</p>

</form>

</section>

</div>

</div>

/Views/Account/Lockout.cshtml


@model object

@{

ViewBag.Title = "LOCKED OUT";

Layout = "_Layout";

}

<h2 class="title is-2">@ViewBag["Title"]</h2>

These views and view models aren’t going to help us without a controller to house business logic.  I created an “AccountController” in the “/Controllers” directory.  There is a lot of code in there – most of it relatively self-explanatory.  We’ll feel better about what it does after the next post on automated testing!  One thing you’ll notice is a new configuration parameter “EnableRegistration” that allows us to disable or enable registration.

/Controllers/AccountController.cs


using System;

using System.Text.Encodings.Web;

using System.Threading.Tasks;

using ExampleWebApplication.Models;

using ExampleWebApplication.Models.ViewModels;

using Microsoft.AspNetCore.Authentication;

using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Http;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Identity.UI.Services;

using Microsoft.AspNetCore.Mvc;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.Logging;

namespace ExampleWebApplication.Controllers

{

public class AccountController : Controller

{

private readonly UserManager<User> _userManager;

private readonly SignInManager<User> _signInManager;

private readonly ILogger _logger;

private readonly IEmailSender _emailSender;

private readonly IConfiguration _configuration;

public IAccountControllerWrappers Wrappers { get; set; }

public AccountController(

UserManager<User> userManager,

SignInManager<User> signInManager,

ILogger<AccountController> logger,

IEmailSender emailSender,

IConfiguration configuration)

{

_userManager = userManager;

_signInManager = signInManager;

_logger = logger;

_emailSender = emailSender;

_configuration = configuration;

Wrappers = new AccountControllerWrappers();

}

[HttpGet]

[AllowAnonymous]

public IActionResult Register(string returnUrl = null)

{

ViewData["ReturnUrl"] = returnUrl;

return View();

}

[HttpPost]

[AllowAnonymous]

[ValidateAntiForgeryToken]

public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)

{

if (!Convert.ToBoolean(_configuration["EnableRegistration"]))

{

// If we got this far, something failed, redisplay form

ViewData["Error"] = "Registration disabled!";

return View();

}

ViewData["ReturnUrl"] = returnUrl;

if (ModelState.IsValid)

{

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

var result = await _userManager.CreateAsync(user, model.Password);

if (result.Succeeded)

{

_logger.LogInformation("User created a new account with password.");

var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);

var callbackUrl = Wrappers.GetActionLink(Url, nameof(ConfirmEmail), user, code);

await _emailSender.SendEmailAsync(model.Email, "Confirm your email",

$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

TempData["Information"] = "Confirmation email sent.";

_logger.LogInformation("User created a new account with password.");

return RedirectToLocal(returnUrl);

}

AddErrors(result);

}

// If we got this far, something failed, redisplay form

return View(model);

}

[HttpGet]

[AllowAnonymous]

public async Task<IActionResult> ConfirmEmail(string userId, string code)

{

var user = await _userManager.FindByIdAsync(userId);

var result = await _userManager.ConfirmEmailAsync(user, code);

if (result.Succeeded)

{

TempData["Information"] = "Registration confirmed!";

return RedirectToAction(nameof(Login));

}

TempData["Error"] = "Something went wrong.";

return RedirectToAction(nameof(Register));

}

[HttpGet]

[AllowAnonymous]

public async Task<IActionResult> Login(string returnUrl = null)

{

// Clear the existing external cookie to ensure a clean login process

await Wrappers.SignOutAsync(HttpContext);

ViewData["ReturnUrl"] = returnUrl;

return View();

}

[HttpPost]

[AllowAnonymous]

[ValidateAntiForgeryToken]

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)

{

ViewData["ReturnUrl"] = returnUrl;

if (ModelState.IsValid)

{

// This doesn't count login failures towards account lockout

// To enable password failures to trigger account lockout, set lockoutOnFailure: true

var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe,

lockoutOnFailure: false);

if (result.Succeeded)

{

_logger.LogInformation("User logged in.");

return RedirectToLocal(returnUrl);

}

if (result.IsLockedOut)

{

_logger.LogWarning("User account locked out.");

return RedirectToAction(nameof(Lockout));

}

else

{

ModelState.AddModelError(string.Empty, "Invalid login attempt.");

return View(model);

}

}

// If we got this far, something failed, redisplay form

return View(model);

}

[HttpPost]

[ValidateAntiForgeryToken]

public async Task<IActionResult> Logout()

{

await _signInManager.SignOutAsync();

_logger.LogInformation("User logged out.");

return RedirectToAction(nameof(HomeController.Index), "Home");

}

[HttpGet]

[AllowAnonymous]

public IActionResult Lockout()

{

return View();

}

private void AddErrors(IdentityResult result)

{

foreach (var error in result.Errors)

{

ModelState.AddModelError(string.Empty, error.Description);

}

}

private IActionResult RedirectToLocal(string returnUrl)

{

if (returnUrl != null && Wrappers.IsLocalUrl(Url, returnUrl))

{

return Redirect(returnUrl);

}

else

{

return RedirectToAction(nameof(HomeController.Index), "Home");

}

}

[HttpGet]

[AllowAnonymous]

public IActionResult ForgotPassword()

{

return View();

}

[HttpPost]

[AllowAnonymous]

[ValidateAntiForgeryToken]

public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)

{

var user = await _userManager.FindByEmailAsync(model.Email);

if (user != null)

{

var code = await _userManager.GeneratePasswordResetTokenAsync(user);

var callbackUrl = Wrappers.GetActionLink(Url, nameof(ResetPassword), user, code);

await _emailSender.SendEmailAsync(model.Email, "Reset your password",

$"Please reset your password by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

TempData["Information"] = "Password reset email sent.";

return RedirectToAction(nameof(Login));

}

TempData["Error"] = "No user with that email address found.";

return RedirectToAction(nameof(Register));

}

[HttpGet]

[AllowAnonymous]

public async Task<IActionResult> ResetPassword(string userId, string code)

{

var user = await _userManager.FindByIdAsync(userId);

if (user != null)

{

var viewModel = new ResetPasswordViewModel {Email = user.Email, Code = code};

return View(viewModel);

}

TempData["Error"] = "Invalid confirmation code.";

return RedirectToAction(nameof(Register));

}

[HttpPost]

[AllowAnonymous]

[ValidateAntiForgeryToken]

public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)

{

var user = await _userManager.FindByEmailAsync(model.Email);

if (user != null)

{

await _userManager.ResetPasswordAsync(user, model.Code, model.Password);

await _emailSender.SendEmailAsync(model.Email, "Password reset notification",

$"Your password has been reset successfully.");

TempData["Information"] = "Password reset successfully.";

return RedirectToAction(nameof(Login));

}

TempData["Error"] = "No user with that email address found.";

return RedirectToAction(nameof(Register));

}

public IActionResult ExternalLogin()

{

throw new NotImplementedException();

}

}

public class AccountControllerWrappers : IAccountControllerWrappers

{

public async Task SignOutAsync(HttpContext context)

{

await context.SignOutAsync(IdentityConstants.ExternalScheme);

}

public string GetActionLink(IUrlHelper helper, string action, User user, string code)

{

return helper.ActionLink(action,

values: new {userId = user.Id, code = code});

}

public bool IsLocalUrl(IUrlHelper helper, string url)

{

return helper.IsLocalUrl(url);

}

}

public interface IAccountControllerWrappers

{

public Task SignOutAsync(HttpContext context);

public string GetActionLink(IUrlHelper helper, string action, User user, string code);

public bool IsLocalUrl(IUrlHelper helper, string url);

}

}

Last, we’ll create a quick view partial to add login and logout functions to our navbar.

/Views/Shared/_LoginPartial.cshtml


@using Microsoft.AspNetCore.Identity

@inject SignInManager<User> SignInManager

@inject UserManager<User> UserManager

<div class="navbar-end">

@if (SignInManager.IsSignedIn(User))

{

<div class="navbar-item">Hello @UserManager.GetUserName(User)!</div>

<div class="navbar-item">

<div class="buttons">

<form asp-controller="Account" asp-action="Logout" method="post">

<button class="button is-link" type="submit">Log out</button>

</form>

</div>

</div>

}

else

{

<div class="navbar-item">

<div class="buttons">

<a class="button is-light" asp-controller="Account" asp-action="Login">Log in</a>

<a class="button is-link" asp-controller="Account" asp-action="Register">Register</a>

</div>

</div>

}

</div>

Now we’ll include the login partial in our layout:

/Views/Shared/_Layout.cshtml


<!DOCTYPE html>

<html lang="en" class="has-navbar-fixed-top">

<head>

<meta charset="utf-8"/>

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>@ViewData["Title"] - Example Web Application</title>

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

</head>

<body>

<header id="navbar-root">

<vue-navbar inline-template>

<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">

<div class="navbar-brand">

<a asp-controller="Home" asp-action="Index" class="navbar-item">

<span>Example Web Application</span>

</a>

<a role="button" v-on:click="toggle" v-bind:class="{ 'is-active': burgerActive }" class="navbar-burger burger" data-target="menuBar">

<span></span>

<span></span>

<span></span>

</a>

</div>

<div id="menuBar" v-bind:class="{ 'is-active': burgerActive }" class="navbar-menu">

<div class="navbar-start">

<a class="navbar-item">

Menu 1

</a>

<a class="navbar-item">

Menu 2

</a>

</div>

@* <--the new line is below *@

@await Html.PartialAsync("_LoginPartial")

</div>

</nav>

</vue-navbar>

</header>

<section>

<div class="container">

<div class="columns">

<div class="column is-10 is-offset-1">

<h1 class="title is-1">@ViewData["Title"]</h1>

<h3 class="title is-3 has-text-info">@ViewData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@ViewData["Error"]</h3>

<h3 class="title is-3 has-text-info">@TempData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@TempData["Error"]</h3>

@RenderBody()

</div>

</div>

</div>

</section>

<footer class="footer">

<div class="content has-text-centered">

&copy; 2020 - yoursitename.com - <a asp-controller="Home" asp-action="Privacy">Privacy</a>

</div>

</footer>

<script src="~/js/layout.js" asp-append-version="true"></script>

</body>

</html>

At this point, we should be able to start the app up and register a new user (assuming your configuration allows that).  To make life easier, let’s seed some test accounts and wire our seed process into the application configuration. This will come in handy during initial development, can be controlled by external configuration, and will help setup automated tests and put the database in known state prior to test executions.

In the “/Models” folder, I’ve create the following file:

/Models/SeedData.cs


using System;

using System.Linq;

using ExampleWebApplication.Data;

using Microsoft.AspNetCore.Identity;

using Microsoft.EntityFrameworkCore;

using Microsoft.Extensions.DependencyInjection;

namespace ExampleWebApplication.Models

{

public static class SeedData

{

public static void Initialize(IServiceProvider serviceProvider)

{

SeedRoles(serviceProvider);

SeedUsers(serviceProvider);

}

private static void SeedUsers(IServiceProvider serviceProvider)

{

var userManager = serviceProvider.GetRequiredService<UserManager<IdentityUser>>();

foreach (var user in userManager.Users.ToList())

{

userManager.DeleteAsync(user).Wait();

}

if (userManager.FindByEmailAsync("<username goes here>").Result == null)

{

IdentityUser user = new IdentityUser();

user.UserName = "<username goes here>";

user.Email = "<email goes here>";

user.EmailConfirmed = true;

IdentityResult result = userManager.CreateAsync(user, "<password goes here>").Result;

if (result.Succeeded)

{

userManager.AddToRoleAsync(user, "Admin").Wait();

}

}

}

private static void SeedRoles(IServiceProvider serviceProvider)

{

var roleManager

= serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

foreach (var role in roleManager.Roles.ToList())

{

roleManager.DeleteAsync(role).Wait();

}

if (!roleManager.RoleExistsAsync("Admin").Result)

{

IdentityRole role = new IdentityRole();

role.Name = "Admin";

IdentityResult roleResult = roleManager.

CreateAsync(role).Result;

}

}

}

}

To finish, I’ll write my SeedData process into my applications startup, after the configuration is available by updating “/Program.cs”.

/Program.cs


using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

using Microsoft.AspNetCore.Hosting;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Microsoft.Extensions.Hosting;

using Microsoft.Extensions.Logging;

namespace ExampleWebApplication

{

public class Program

{

public static void Main(string[] args)

{

var host = CreateHostBuilder(args).Build();

using (var scope = host.Services.CreateScope())

{

var serviceProvider = scope.ServiceProvider;

try

{

SeedData.Initialize(serviceProvider);

}

catch (Exception ex)

{

var logger = serviceProvider.GetRequiredService<ILogger<Program>>();

logger.LogError(ex, "An error occurred seeding the DB.");

}

}

 

host.Run();

}

public static IHostBuilder CreateHostBuilder(string[] args) =>

Host.CreateDefaultBuilder(args)

.ConfigureWebHostDefaults(webBuilder =>

{

webBuilder.UseStartup<Startup>();

});

}

}

 

That’s it!  In the next post, we’ll cover our new controller with some automated tests and make sure all of the logic branches are functioning properly and we understand what steps the logic is taking.

Building an ASP.NET Core Starter App on MacOS (Webpack & NPM)

In the previous post, I quickly covered how to get started in a way that smoothed some future bumps in the road for me.  In this post, we’ll cover replacing the default front-end frameworks and add tooling to support front-end dependency management and static asset assembly.  An example of the configuration mentioned in this post (along with the source code for all future posts) is available at: https://github.com/jrodenbostel/example-web-application.

If you followed the tutorial mentioned in the previous post, chances are you generated an application similar to the result of running `dotnet new mvc …` from the CLI.  If you did, you’re mostly looking at a reasonable shell of an application.  In my opinion, there are some issues with how the front-end tools are made available that are worth remediating.  The two big items are there’s no tool being used to manage front-end dependencies like jQuery and Bootstrap, and most if not all of the files in the wwwroot folder could be considered build artifacts or transient dependencies.  In most cases, build artifacts and dependencies shouldn’t be checked into version control, but rather assembled or retrieved at build time. Here’s a some background information for those unfamiliar with this concept: https://stackoverflow.com/questions/31930370/what-is-npm-and-why-do-i-need-it. I’m not a front-end expert by any means, but I appreciate being able to join a team and quickly get up and running, and I appreciate the consistency and speed of automated build and deployment processes.

Just to change things up for this project, I’ll be using Vue.js (https://vuejs.org/) instead of jQuery and Bulma (https://bulma.io/) instead of Bootstrap.  We’ll change our project to use Sass (https://sass-lang.com/) instead of plain CSS.  We’ll remove the defaults and manage all those things with NPM (https://www.npmjs.com/) and Webpack (https://webpack.js.org/).  We’ll also plug the new tools into the default Layout.

If you’re looking at a freshly generated MVC application, you’ll see the `wwwroot` folder along with subfolders `css`, `js`, and `lib`, and also you’ll also see there are files in each of those folders as well.  These files are referenced in the generated layout (`/Views/Shared/_Layout.cshtml`).

1

I started by deleting everything in the wwwroot folder, including the subfolders.  I created a .gitignore file in the root of my project and added the following entries:

2

If you’ve already made some commits to git, you might have to remove these files from git in addition to deleting them.  I should also note that since this is currently a single project solution, and since I am a Rider user, the following entries are also useful:

3

NPM

In the previous post, we covered installing node.js as a prerequisite to getting the example app up and running.  In this section, we’re going to use the Node Package Manager (NPM) to install all the dependencies we’ll need for this tutorial.  While I hope this helps readers get started quickly, it’s always a good idea to take some time to understand what each of these dependencies does to ensure that you’re not installing anything unnecessary.

First, we’ll create a `package.json` file where all of our dependencies will be recorded.  Soon, we’ll also use this file to set up some automated scripts that leverage our dependencies.  Create `package.json` in the root of your project with the following contents:


{

"name": "example-web-application",

"version": "1.0.0",

"description": "",

}

Next, we’ll install all the tools we need to get a reasonable static asset assembly process setup – specifically, this will install tools needed to incorporate Sass and Webpack into the project.  Run this command from the root of your project:

`npm install css-loader extract-text-webpack-plugin file-loader html-webpack-plugin mini-css-extract-plugin minimist node-sass optimize-css-assets-webpack-plugin sass-loader url-loader webpack webpack-cli webpack-merge –save-dev`

When that is complete, the following path should be valid in your filesystem `<project_root>/node_modules`, and the node_modules folder should have about 83MB  of files in it.  You should also see your package.json file updated to include a listing of the files we just installed.  We’ll add more dependencies later, but this will help us get Webpack configured appropriately.

Webpack

Before we go further, I know for sure I had some success following this tutorial: https://webpack.js.org/guides/getting-started/.  I know for sure that I followed others.  I was not able to find which webpack tutorial I followed in my notes.  With that out of the way, we need a few other things to get started with Webpack.  First we’ll start with the Webpack config.  We’re going to create three files:

`webpack.common.js`: common configuration

`webpack.dev.js`: configuration for ‘development’ environments

`webpack.prod.js`: configuration for ‘production’ environments

webpack.common.js

This file contains file loader plugin configuration (for fonts), and has references to file locations.


var path = require('path');

const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

 

module.exports = {

entry: {

app: './Scripts/app.js'

},

output: {

path: path.resolve(__dirname, 'wwwroot'),

filename: 'js/[name].js',

publicPath: '/'

},

plugins: [

new MiniCssExtractPlugin({

filename: 'css/site.css',

chunkFilename: '[name].css'

})

],

module: {

rules: [

{

test: /\.(png|jpg|jpeg|gif|woff|woff2|ttf|eot|svg)(\?.*)?$/,

loader: 'file-loader?name=fonts/[name].[ext]',

},

{

test: /\.s[ac]ss$/i,

use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],

},

]

}

};

 

webpack.dev.js

The contents of this file will be combined with `webpack.common.js` when executed. This file contains a reference to the ‘inline-source-map’ devtool configuration, which is a Webpack option for copying files as-is from location to location ie without minifying or obfuscating them.  This configuration combined with the common configuration should result in files being copied from `/Scripts` to their homes in ‘wwwroot’ with the only transformation being css extracted from the scss/sass files in `/Scripts`.map’ devtool configuration, which is a Webpack option for copying files as-is from location to location ie not   We’ll cover how to run these commands shortly.


const webpack = require('webpack');

const merge = require('webpack-merge');

const common = require('./webpack.common.js');

 

module.exports = merge(common, {

mode: 'development',

devtool: 'inline-source-map'

});

 

webpack.prod.js

The contents of this file will also be combined with `webpack.common.js` when executed.  This file contains references to Webpack plugins used to minify and obfuscate files for optimized loading.  This configuration combined with the common configuration should result in file being copied from `/Scripts` to their homes in `wwwroot`, but this time, they’ll be optimized by the aforementioned plugins.


const webpack = require('webpack');

const merge = require('webpack-merge');

const common = require('./webpack.common.js');

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const TerserJSPlugin = require('terser-webpack-plugin');

 

module.exports = merge(common, {

mode: 'production',

optimization: {

minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

}

});

 

Execution Convenience

Now that we have Webpack configured, we need to be able to test it.  Before we test it, we’ll set up an easy way to execute but adding the following entry to the root of our `package.json` file:

“scripts”: {

“dev”: “webpack –config webpack.dev.js –watch”,

“build”: “webpack –config webpack.prod.js”

}

Now, from the root of our project folder (the same folder where `package.json` is located), we can run:

`npm run dev` which runs our dev configuration, and watches the filesystem for changes.  When changes are detected, Webpack is run again with the same configuration. This is useful while in active development as keeping this running in the background will keep you assets building and bundled as you go.

`npm run build` which runs our prod configuration, and is useful when deploying to a shared environment. It executes once and will not run again upon completion.

Next let’s add Vue and Bulma as dependencies to our project and test our Webpack configuration.

From the root of your project, run the following command to install Bulma and Vue as dependencies of our project:


npm install bulma vue --save-dev

Now, let’s setup a JavaScript file and a Sass file for our project and test out our Webpack config.  Note that the file names and paths below are referenced in our Webpack config.

/Scripts/app.js


require('./site.scss');

import Vue from 'vue/dist/vue';

 

/Scripts/site.scss


@charset "utf-8";

 

@import "~bulma/bulma";

 

When we run ‘npm run dev’ or ‘npm run build’ from the root of our project, we should see files appear in wwwroot.

You’ll want to update your `/Views/Shared/_Layout.cshtml` to include references to the new files, and remove references to what was previously there.  You’ll see examples of that in the file below:

/Views/Shared/_Layout.cshtml


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>@ViewData["Title"] - ExampleWebApplication</title>

<link rel="stylesheet" href="~/css/site.css" />

</head>

<body>

<header>

<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">

<div class="container">

<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ExampleWebApplication</a>

<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"

aria-expanded="false" aria-label="Toggle navigation">

<span class="navbar-toggler-icon"></span>

</button>

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">

<ul class="navbar-nav flex-grow-1">

<li class="nav-item">

<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>

</li>

<li class="nav-item">

<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>

</li>

</ul>

</div>

</div>

</nav>

</header>

<div class="container">

<main role="main" class="pb-3">

@RenderBody()

</main>

</div>

 

<footer class="border-top footer text-muted">

<div class="container">

&copy; 2020 - ExampleWebApplication - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>

</div>

</footer>

<script src="~/js/app.js" asp-append-version="true"></script>

</body>

</html>

Last, you’ll probably want to re-style your layout to use Bulma classes, and I’ve added a quick Vue component to handle toggling the navbar styles based on the viewport size.  Here’s an update:


<!DOCTYPE html>

<html lang="en" class="has-navbar-fixed-top">

<head>

<meta charset="utf-8"/>

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>@ViewData["Title"] - Example Web Application</title>

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

</head>

<body>

<header id="navbar-root">

<vue-navbar inline-template>

<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">

<div class="navbar-brand">

<a asp-controller="Home" asp-action="Index" class="navbar-item">

<span>Example Web Application</span>

</a>

<a role="button" v-on:click="toggle" v-bind:class="{ 'is-active': burgerActive }" class="navbar-burger burger" data-target="menuBar">

<span></span>

<span></span>

<span></span>

</a>

</div>

<div id="menuBar" v-bind:class="{ 'is-active': burgerActive }" class="navbar-menu">

<div class="navbar-start">

<a class="navbar-item">

Menu 1

</a>

<a class="navbar-item">

Menu 2

</a>

</div>

</div>

</nav>

</vue-navbar>

</header>

<section>

<div class="container">

<div class="columns">

<div class="column is-10 is-offset-1">

<h1 class="title is-1">@ViewData["Title"]</h1>

<h3 class="title is-3 has-text-info">@ViewData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@ViewData["Error"]</h3>

<h3 class="title is-3 has-text-info">@TempData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@TempData["Error"]</h3>

@RenderBody()

</div>

</div>

 

</div>

</section>

 

<footer class="footer">

<div class="content has-text-centered">

&copy; 2020 - yoursitename.com - <a asp-controller="Home" asp-action="Privacy">Privacy</a>

</div>

</footer>

<script src="~/js/layout.js" asp-append-version="true"></script>

</body>

</html>

We’ll also need new Javascript to handle some of the Vue directives referenced in the previous file:

/Scripts/app.js


require('./site.scss');

 

import Vue from 'vue/dist/vue';

 

Vue.component('vue-navbar', {

data() {

return {

width: 0,

height: 0,

burgerActive: false

}

},

methods: {

toggle(e) {

this.burgerActive = !this.burgerActive;

},

handleResize() {

this.width = window.innerWidth;

this.height = window.innerHeight;

this.burgerActive = false;

}

},

created() {

window.addEventListener('resize', this.handleResize);

this.handleResize();

},

destroyed() {

window.removeEventListener('resize', this.handleResize);

}

});

 

new Vue({

el: '#navbar-root'

});

 

At this point, we should have a working Webpack config, Vue and Bulma installed and incorporated into our layout, and a simple Vue component that helps keep the navbar responsive when the viewport size is reduced.

4

In the next post, I’ll focus much more on code, covering adding custom authentication to the project.

Building an ASP.NET Core Starter App on MacOS (Getting Started)

In this post, I’ll cover how I got started, first installing the prerequisites and then workarounds I used while following Microsoft documentation.  At the conclusion of this post, you’ll have achieved the same outcome as this Microsoft “Getting Started” tutorial (https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/?view=aspnetcore-3.1), but without having to deal with some of the shortcomings common in tutorials, such as the inconveniences and lack of support in Entity Framework for SQLite.  This part of the journey was fairly straightforward.  Following the tutorial was perfectly fine. The deviations I’ve outlined in this post helped me move beyond a ‘tutorial-grade’ solution and get to a codebase more well-suited to tackle the subject matter covered in future parts of this blog series.

Assuming you’re starting from scratch, you’ll need to have the .NET SDK installed, and you’ll likely want to have node/npm installed in a manageable way as well.

I’m a homebrew person myself.  I installed the .NET SDK using this formula: https://formulae.brew.sh/cask/dotnet-sdk and the ​brew install dotnet-sdk command.

It’s easy to slowly acquire an out-of-control web of node dependencies over the course of many projects, just like it was with Ruby gems.  For this reason, I prefer installing and managing node versions for each project using NVM: https://github.com/nvm-sh/nvm. Installing node isn’t mentioned in the Microsoft tutorial, but will be helpful in the subsequent posts in this series.

The Microsoft documentation for getting started takes you pretty easily through most of the steps regardless of the IDE you’re using or the database you’re planning on connecting to: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/working-with-sql?view=aspnetcore-3.1&tabs=visual-studio . With a little creativity, and some help from Azure, you can follow the instructions that rely on SQL Server.  The issue you’ll run into on MacOS is that there is no version of “SQL Server Express LocalDB” and the documentation suggests using SQLite.  The problem with SQLite (as noted in the documentation in the ‘Working with a database’ section using the ‘Visual Studio Code/Visual Studio for Mac’ instructions) is that any Entity Framework migrations that include dropping or altering a column will fail when run against SQLite.  This likely won’t be an issue in the short term, but more than likely will be in the long term.

Using MySQL: (https://dev.mysql.com/doc/connector-net/en/connector-net-entityframework60.html)

Using Postgres: (https://npgsql.org/efcore)

Since I will eventually be deploying my app to Azure I will use Azure SQL (which can be done for less than $5/mo).

Using Azure: Assuming you have an Azure account, from the Azure Portal, Go to ‘Create a resource’ -> ‘New’ -> ‘Databases’ -> ‘SQL Database’Screen Shot 2020-05-11 at 2.17.47 PM

After choosing a subscription, resource group, and entering a name and (or creating) a server, THE COST OF THE TYPE OF DATABASE CAN VARY GREATLY!  In the ‘Compute + storage’ section, click ‘Configure Database’.

Note that if you do not have an available server to configure your database on, you can configure one from the ‘Basics’ tab using the ‘Create new’ option under the ‘Server’ drop down.  If you decide to create new, you are presented with a ‘New server’ dialog on the right side of your browser window.  From there, you can choose a server name, admin account name and password.

Screen Shot 2020-05-11 at 2.45.08 PM

I’ve been using the ‘Basic’ configuration, that includes 5 DTUs and 2GB of storage.  That’s been consistently $5/mo. There may be cheaper options using serverless configuration.  There are DEFINITELY more expensive options.  The important thing is to make sure to understand what you’re signing up for and the potential expense.

Screen Shot 2020-05-11 at 2.45.46 PM

I clicked ‘Next’ to get to the ‘Networking’ step and made no changes.

I clicked ‘Next’ to get to the ‘Additional Settings’ step and made no changes.

I clicked ‘Next’ to get to the ‘Tags’ page and made no changes.

I clicked ‘Create’ to complete the setup.

Now from the Azure Portal, you can navigate to ‘Home’ > ‘SQL databases’, and choose the database you just created.  From there, you can do useful things like configure the firewall to use whitelisted IP addresses,

Screen Shot 2020-05-11 at 2.47.20 PM

and you can retrieve your connection string.  This is where I retrieved the connection string for my local appsettings.json file.

Screen Shot 2020-05-11 at 2.47.15 PM

Now that we’ve got a solid foundation to work with, in the next post, I’ll cover swapping out default front-end frameworks and adding front-end dependency management and static asset packaging.

NOTE: It’s probably worth mentioning before we go further that I haven’t had great luck with Visual Studio for Mac (https://visualstudio.microsoft.com/vs/mac/).  I have had much enjoyable experience with VS Code (https://code.visualstudio.com/) and Rider (https://www.jetbrains.com/rider/).  For the rest of the posts in this series, if you see a screen shot of an IDE, it will be a screenshot of Rider.

Building an ASP.NET Core Starter App on MacOS (Intro)

I teach as a side gig to my day job at SPR.  It’s just one class at the university that I attended.  This past spring semester was my second attempt.  The whole experience is very rewarding (obviously), but what really makes it special is that it’s a section of the capstone for the Computer Science department – the last class the students have to take (and pass) in order to graduate.  The students plan a project for a real client in the fall and build it in the spring.  Clients are generally small businesses, non-profit/not-for-profit organizations, or charities.  The business problem and technologies change each semester.

This past semester we built an app intended to be used by the university’s theater department to inventory costumes and set pieces for the different shows they perform.  Eventually, the goal was to share the app with other universities and local theater groups so that when costumes and set pieces are lent/borrowed between organizations they are more easily tracked.  We built this project using ASP.NET Core MVC.  As someone with very little C#/.NET development experience, and with myself and several others in the class using Apple laptops as development machines, we ran into some interesting challenges and addressed what I perceived to be shortcomings in the Microsoft tutorials and starter app examples.  We pieced together many solutions that I feel are worth sharing for others getting started in ASP.NET Core development outside of the traditional Windows/Visual Studio development toolchain and/or are used to some of the pre-configured tooling of other frameworks.

This post is the first in a series that will cover details and examples of:

  • getting a project up and running
  • swapping out default tools (replacing jQuery and Bootstrap with Vue.js and Bulma)
    • building simple front-end components within Razor templates using Vue.js
  • building testable/customizable authentication and authorization components
  • Integration/unit test configuration

This isn’t an endorsement of the tools that we used to replace the defaults.  Any tools can be used – including the defaults.  My goal in writing this series is to share what we learned when moving away from defaults and how we built the pieces of what we consider a more robust “starter app”, one more conducive to modern developer workflow.  We also happened to do a significant amount of work outside the typical toolchain and learned a bunch of lessons for that reason as well.  If you’re using an alternative development platform and figuring out where to start when building an app using tools/frameworks other than the defaults, this series is for you.

First, how we got here…

Visual Studio for Mac just doesn’t have the same functionality as Visual Studio for Windows.  I understand why, but it seems many tutorials involve code generation using options not available outside of Visual Studio for Windows.  This includes Visual Studio for Mac, but also the dotnet command line tools.  A great example is scaffolding authentication and authorization with overrides for custom views.

I wasn’t able to find a lot of material on unit testing services and other code that relies on Identity and Entity Framework components.  Specifically difficult was locating information on mocking.  I was able to find tutorials for integration testing, but even then functionality I’m used to from other frameworks (migrations before test executions, configuration of set-up and tear-down through annotations, etc) required a decent amount of configuration.

A scaffolded ASP.NET Core MVC app doesn’t include any tooling for relatively modern front-end development including managing front-end dependencies and assembly/minification of static assets.

Before we get started, like many other posts, I stood on the shoulders of many giants to build these examples.  I’ll include links to their sites/posts and standard documentation wherever I can.  Thanks to all those people for all their work.  Hopefully this series will be another meaningful contribution to the community.  Code supporting these posts is located here: https://github.com/jrodenbostel/example-web-application.