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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s