Category Archives: Asp.NET Core

Building a list with Asp.Net Core

I’ve recently been working with Asp.Net Core to build some functionality, involving building a list of values. Typically, with Asp.Net Core using Razor, you have a form that may look something like this:

@using (Html.BeginForm("MyAction", "ControllerName", FormMethod.Post)
{
    @Html.AntiForgeryToken()
    <div class="form-group">
        @Html.LabelFor(model => model.MyValue)
        @Html.TextBoxFor(model => model.MyValue)
    </div>

    <div class="form-group">
        <button type="submit">Submit</button>
    </div>

This works really well in 90% of cases, where you want the user to enter a value and submit. This is your average CRUD application; however, what happens if, for some reason, you need to manipulate one of these values? Let’s say, for example, that you want to submit a list of values.

For the sake of simplicity, we’ll say that the controller accepts a csv, but we want to build this up before submission. You can’t simply call a controller method for two reasons: the first is that the controller will reload the page; and the second that you don’t have anywhere to put the value on the server. If this was, say, a method to create an entry in the DB, the DB entry, by definition, couldn’t exist until after the submission.

This all means that you would need to build this list on the client.

A solution

Let’s start with a very simple little feature of Html Helpers – the hidden field:

@Html.HiddenFor(model => model.MyList)

This means that we can store the value being submitted to the user, without showing it to the user.

We’ll now need to display the data being added. An easy way to do this is a very simple table (you can load existing values into the table for edit scenarios):

    <div>
        <table id="listTable">
            <tbody>
                @if ((Model?.ValueList ?? null) != null)
                {
                    @foreach (var v in Model.ValueList)
                    {
                        <tr>
                            <td>@v</td>
                        </tr>
                    }
                }
            </tbody>
        </table>
    </div>    

Pay particular attention to the Table Id and the fact that the conditional check is inside the tbody tag. Now let’s allow the user to add a new piece of data:

    <div class="form-group">
        @Html.LabelFor(model => model.NewValue)
        @Html.TextBoxFor(model => model.NewValue)
    </div>
    <div>
        <button type="button" id="add-value">Add Value</button>
    </div>

Okay, so now we have a button and a field to add the value; we also have a method of displaying those values. We’ll need a little bit of Javascript (JQuery in this case) to append to our list:

@section Scripts {
        $('#add-value').click(() => {

            const hiddenList = $('#MyList');
            const newValue = $('#NewValue');

            if (!hiddenList.val()) {
                hiddenList.val(newValue.val());
            } else {
                hiddenList.val(hiddenList.val() + ',' + newValue.val());
            }
            
            $('#listTable > tbody:last-child').append('<tr><td>' + newValue.val() + '</td></tr>');            
        });

On the button click, we get the hidden list and the new value, we then simply add the new value to the list. Finally, we manipulate the table in order to display the new value. If you F12 the page, you’ll notice that the Razor engine replaces the Html Helpers with controls that have Ids the same as the fields that they are displaying (note that if the field name contains a “.”, for example: MyClass.MyField, the Id would be MyClass_MyField).

When you now submit this, you’ll see that the hidden field contains the correct list of values.

References

https://stackoverflow.com/questions/16174465/how-do-i-update-a-model-value-in-javascript-in-a-razor-view/16174926

https://stackoverflow.com/questions/171027/add-table-row-in-jquery

https://stackoverflow.com/questions/36317362/how-to-add-an-item-to-a-list-in-a-viewmodel-using-razor-and-net-core

Policy Server with Asp.Net Core

It took me a while to collate my notes for this; hopefully this will be the first in a vaguely related series on using Policy Server.

Policy Server is a product from a company called Solliance, allowing you to control authorisation within your application (as opposed to authentication – the same company produces a sister product, called Identity Server). The idea is that, the user is authenticated to determine they are who they say they are, but then authorised to use certain parts of the application, or to do certain things.

This post discusses setting up the open source version of Policy Server. There is a commercial version of this.

The ReadMe on the above GitHub page is quite extensive, however, when I was trying this out, I had some areas that I thought weren’t too clear. This post will explain that to me in the future, should I become unclear again (a common occurrence when you get to a certain age!)

It’s worth bearing in mind that authorisation and authentication need to be done on the server; whilst you may choose to also do that on the client, the code on the client is, well, on the client! If you have some Javascript code sat in the web browser that’s stopping me from seeing the defence plans, then that’s just not secure enough.

Installing and Configuring Policy Server

The first step here is to install the NuGet package:

Install-Package PolicyServer.Local

Next, add Policy Server in startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            // In production, the React files will be served from this directory
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/build";
            });

            services.AddPolicyServerClient(Configuration.GetSection("Policy"));
    }

Amazingly, at this stage, you’re basically done. The next stage is to add the rules into the appsettings.json. There is an example on the page above, but here’s a much simpler one:

  "Policy": {
    "roles": [
      {
        "name": "salesmanager",
        "subjects": [ "1" ]
      },
      {
        "name": "driver",
        "subjects": [ "2" ]
      }
    ],
    "permissions": [
      {
        "name": "ViewRoutes",
        "roles": [ "driver" ]
      },
      {
        "name": "CreateSalesOrder",
        "roles": [ "salesmanager" ]
      },
      {
        "name": "Timesheet",
        "roles": [ "salesmanager", "driver" ]
      }
    ]
  }

This is, in fact, the part that I didn’t feel was clear enough in the GitHub readme. What, exactly, are “subjects”, and how do I associate them to anything useful? I’ll come back to this shortly, but first, let’s see the rules that we have set up here:

The Sales Manager can create a sales order, and complete their timesheet.

The Driver can view routes and complete their timesheet.

The Sales Manager can not view routes; nor can the driver create a sales order.

Add Policy Server to a Controller

The next stage is to inject Policy Server into our controller. For the home controller, all you need to do is inject IPolicyServerRuntimeClient:

        private readonly IPolicyServerRuntimeClient _policyServerRuntimeClient;

        public HomeController(IPolicyServerRuntimeClient policyServerRuntimeClient)
        {
            _policyServerRuntimeClient = policyServerRuntimeClient;
        }

This can now be used anywhere in the controller. However, for testing purposes, let’s just circle back round to the subject question. The subject is the identity of the user. You may have noticed that I’ve started this post with PolicyServer, so I don’t have any authentication in place here. However, for testing purposes, we can force our user to a specific identity.

Let’s override the Index method of the HomeController:

        public async Task<IActionResult> Index()
        {
            var identity = new ClaimsIdentity(new Claim[]
            {
                new Claim("sub", "2"),
                new Claim("name", "TestUser")
            }, "test");

            User.AddIdentity(identity);

            var homeViewModel = await SecureViewModel();
            return View(homeViewModel);
        }

We’ll come back to the ViewModel business shortly, but let’s focus on the new ClaimsIdentity; notice that the “sub” (or subject) refers to the Subject ID of the driver above. So what we’ve done is created a driver!

The SecureViewModel method returns a ViewModel (which, as you can see, is passed to the view); let’s see what that might look like:

        public async Task<HomeViewModel> SecureViewModel()
        {
            var homeViewModel = new HomeViewModel();            
            homeViewModel.ViewRoutes = await _policyServerRuntimeClient.HasPermissionAsync(User, "ViewRoutes");
            homeViewModel.CreateSalesOrder = await _policyServerRuntimeClient.HasPermissionAsync(User, "CreateSalesOrder");
            homeViewModel.Timesheet = await _policyServerRuntimeClient.HasPermissionAsync(User, "Timesheet");

            return homeViewModel;
        }

You can play about with how this works by altering the “sub” claim.

How does this fit into Identity Server (or an identity server such as Azure B2C)?

The identity result of the authentication, should map to the “sub” claim. In some cases, it’s “nameidentitifier”. Once you’ve captured that, you’ll need to store that reference against the user record.

That’s all very well for testing, but when I use this for real, I want to plug my data in from a database

The first thing you’ll need to do is to ‘flatten’ the data. this StackOverflow question was about the best resource I could find for this, and it gets you about 90% of the way there. I hope to come back to that, and linking this in with Asp.Net Core Policies in the next post.

References

https://auth0.com/blog/role-based-access-control-rbac-and-react-apps/

https://github.com/IdentityServer/IdentityServer4/issues/2968

https://stackoverflow.com/questions/55450393/persist-entitys-in-database-instead-of-appsettings-json

https://stackoverflow.com/questions/55450393/persist-entitys-in-database-instead-of-appsettings-json

.Net Client Side Libraries

In this post, I introduced (to me) a new method of installing client side Javascript libraries for a .Net Web Application.

In this follow-up, I’m going to cover how this can be used in conjunction with npm in order to simply move the files into the correct location. Note that this is a new tool in Visual Studio, and you’ll need VS 15.8.0 or higher for it to work. Let’s start by creating an empty .Net Core Web App. Once you’ve done so, you’re app probably looks like this:

Now you’ll need to launch a bash or powershell console:

npm init
npm install jquery

Refresh your project and show all files:

As you can see, you now have a node_modules directory. The problem is that the files need to be under wwwroot. Obviously, you could copy it, but let’s try the client side manager:

In the resultant dialog, select File System; navigate to the JQuery directory and select the files that you want:

Now refresh the project again and let’s see what we have:

This actually translates to a JSON file; libman.json looks like this:

{
  "version": "1.0",
  "defaultProvider": "filesystem",
  "libraries": [
    {
      "library": "node_modules\\jquery\\dist\\",
      "destination": "wwwroot/lib/dist/",
      "files": [
        "jquery.min.js"
      ]
    }
  ]
}

Abusing the system

Okay – so that’s very nice; but now that we have a system that will copy files from wherever into wherever (the rule is that you have to go deeper in the files structure – so you couldn’t copy that file back again) then we, presumably, could use this for other things. For example, have you ever had a file (an image or something) that you need to include in your project:

Obviously, c:\tmp above could just as easily be \\shareddrive\images.

To be clear, I’m unsure whether this is abusing the system, or it was meant for this exact thing. It is possible that this type of usage may become unsupported in future (so you’ve been warned).

Adding Identity Capabilities to an ASP.NET Core App

If you create a new ASP.Net application, you get a built-in log-in feature – it provides the log-in page, all the back end services and even the DB tables. It does assume that your DB and your web-site are physically located on the same server (or at least that the web site can directly access the DB). Asp.Net Core also provides this, but it’s slightly different. It does still use Entity Framework (Core), and it does still assume direct access to the DB.

For a new application

Adding this functionality to a new application is very straightforward…

Step One – Create a new Asp.Net Core Web App

Step Two – Add authentication

Select “Change Authentication”:

If you’re creating a standard self-authenticating web page, then Individual is the answer. “Windows Authentication” allows you to defer authentication to your domain, and “Work or School Account” allows you to use Microsoft’s own security using AD, Azure or Office 365.

Step Three – Log-in

Now, just log-in:

So far so good; but what if you have already created a web app using ASP.Net Core and want to retrospectively fit this functionality?

For Existing Applications

Obviously, adding this functionality can depend on what you’re adding it to. The following was compiled from an ASP.Net Core app created without identity services, and then retrofitted with them. In order to do this, I strongly recommend starting with a dummy app created as above, as there’s a lot of cutting and pasting coming up.

Step One – Add Entity Framework

The identity service is built on top of EF (Core in this case); so add:

Microsoft.AspNetCore.Identity.EntityFrameworkCore

Step Two – The ApplicationUser Model

You need to add the concept of IdentityUser to your application to use the ASP.Net Core Identity functionality; so you will need a model to represent your user:

This should inherit from IdentityUser:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MyWebApp.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
}

Step Three – ApplicationDbContext

You need a DBContext; this provides an abstraction for EF and allows it to work out how to create your DB, etc.; create a Data folder:

And add a class similar to the following:

using MyWebApp.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MyWebApp.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }
}

Step Four – Startup.cs

With ASP.Net Core there is an opt-in policy; so all the functionality that you might need is registered in an IoC first (including MVC). The identity service needs to be registered in Startup.ConfigureServices:

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            // Add framework services.
            services.AddMvc();
        }

Step Five – Services

To deal with two factor authentication, you’ll need an implementation of a message sender. I initially became confused with this naming, and it refers to a class that sends messages (e-mails, etc), and not message in any of the many other senses you may imagine.

    public interface IEmailSender
    {
        Task SendEmailAsync(string email, string subject, string message);
    }
    public interface ISmsSender
    {
        Task SendSmsAsync(string number, string message);
    }
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public Task SendEmailAsync(string email, string subject, string message)
        {
            // Plug in your email service here to send an email.
            return Task.FromResult(0);
        }

        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            return Task.FromResult(0);
        }
    }

Step Six – ViewModels and Views

I won’t detail them all here, but you’ll need view models and views to cover all the basic functionality (register, reset, login, etc…):

Step Seven – AccountController

The controllers are the drivers for functionality in MVC; the following details how the log-in system will function.

    [Authorize]
    public class AccountController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly IEmailSender _emailSender;
        private readonly ISmsSender _smsSender;
        private readonly ILogger _logger;
        private readonly string _externalCookieScheme;

        public AccountController(
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            IOptions<IdentityCookieOptions> identityCookieOptions,
            IEmailSender emailSender,
            ISmsSender smsSender,
            ILoggerFactory loggerFactory)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
            _emailSender = emailSender;
            _smsSender = smsSender;
            _logger = loggerFactory.CreateLogger<AccountController>();
        }

        //
        // GET: /Account/Login
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> Login(string returnUrl = null)
        {
            // Clear the existing external cookie to ensure a clean login process
            await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);

            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        //
        // POST: /Account/Login
        [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(1, "User logged in.");
                    return RedirectToLocal(returnUrl);
                }
                if (result.RequiresTwoFactor)
                {
                    return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
                }
                if (result.IsLockedOut)
                {
                    _logger.LogWarning(2, "User account locked out.");
                    return View("Lockout");
                }
                else
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return View(model);
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        //
        // GET: /Account/Register
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Register(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        //
        // POST: /Account/Register
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
                    // Send an email with this link
                    //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                    //var callbackUrl = Url.Action(nameof(ConfirmEmail), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
                    //await _emailSender.SendEmailAsync(model.Email, "Confirm your account",
                    //    $"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>");
                    await _signInManager.SignInAsync(user, isPersistent: false);
                    _logger.LogInformation(3, "User created a new account with password.");
                    return RedirectToLocal(returnUrl);
                }
                AddErrors(result);
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        //
        // POST: /Account/Logout
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Logout()
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation(4, "User logged out.");
            return RedirectToAction(nameof(HomeController.Index), "Home");
        }

        //
        // POST: /Account/ExternalLogin
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public IActionResult ExternalLogin(string provider, string returnUrl = null)
        {
            // Request a redirect to the external login provider.
            var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { ReturnUrl = returnUrl });
            var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            return Challenge(properties, provider);
        }

        //
        // GET: /Account/ExternalLoginCallback
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
        {
            if (remoteError != null)
            {
                ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
                return View(nameof(Login));
            }
            var info = await _signInManager.GetExternalLoginInfoAsync();
            if (info == null)
            {
                return RedirectToAction(nameof(Login));
            }

            // Sign in the user with this external login provider if the user already has a login.
            var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
            if (result.Succeeded)
            {
                _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
                return RedirectToLocal(returnUrl);
            }
            if (result.RequiresTwoFactor)
            {
                return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
            }
            if (result.IsLockedOut)
            {
                return View("Lockout");
            }
            else
            {
                // If the user does not have an account, then ask the user to create an account.
                ViewData["ReturnUrl"] = returnUrl;
                ViewData["LoginProvider"] = info.LoginProvider;
                var email = info.Principal.FindFirstValue(ClaimTypes.Email);
                return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
            }
        }

        //
        // POST: /Account/ExternalLoginConfirmation
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
        {
            if (ModelState.IsValid)
            {
                // Get the information about the user from the external login provider
                var info = await _signInManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    return View("ExternalLoginFailure");
                }
                var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
                var result = await _userManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    result = await _userManager.AddLoginAsync(user, info);
                    if (result.Succeeded)
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);
                        _logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider);
                        return RedirectToLocal(returnUrl);
                    }
                }
                AddErrors(result);
            }

            ViewData["ReturnUrl"] = returnUrl;
            return View(model);
        }

        // GET: /Account/ConfirmEmail
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> ConfirmEmail(string userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var user = await _userManager.FindByIdAsync(userId);
            if (user == null)
            {
                return View("Error");
            }
            var result = await _userManager.ConfirmEmailAsync(user, code);
            return View(result.Succeeded ? "ConfirmEmail" : "Error");
        }

        //
        // GET: /Account/ForgotPassword
        [HttpGet]
        [AllowAnonymous]
        public IActionResult ForgotPassword()
        {
            return View();
        }

        //
        // POST: /Account/ForgotPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByEmailAsync(model.Email);
                if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
                {
                    // Don't reveal that the user does not exist or is not confirmed
                    return View("ForgotPasswordConfirmation");
                }

                // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713
                // Send an email with this link
                //var code = await _userManager.GeneratePasswordResetTokenAsync(user);
                //var callbackUrl = Url.Action(nameof(ResetPassword), "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
                //await _emailSender.SendEmailAsync(model.Email, "Reset Password",
                //   $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
                //return View("ForgotPasswordConfirmation");
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }

        //
        // GET: /Account/ForgotPasswordConfirmation
        [HttpGet]
        [AllowAnonymous]
        public IActionResult ForgotPasswordConfirmation()
        {
            return View();
        }

        //
        // GET: /Account/ResetPassword
        [HttpGet]
        [AllowAnonymous]
        public IActionResult ResetPassword(string code = null)
        {
            return code == null ? View("Error") : View();
        }

        //
        // POST: /Account/ResetPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
            }
            var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction(nameof(AccountController.ResetPasswordConfirmation), "Account");
            }
            AddErrors(result);
            return View();
        }

        //
        // GET: /Account/ResetPasswordConfirmation
        [HttpGet]
        [AllowAnonymous]
        public IActionResult ResetPasswordConfirmation()
        {
            return View();
        }

        //
        // GET: /Account/SendCode
        [HttpGet]
        [AllowAnonymous]
        public async Task<ActionResult> SendCode(string returnUrl = null, bool rememberMe = false)
        {
            var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (user == null)
            {
                return View("Error");
            }
            var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
            var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
            return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe });
        }

        //
        // POST: /Account/SendCode
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> SendCode(SendCodeViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }

            var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (user == null)
            {
                return View("Error");
            }

            // Generate the token and send it
            var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider);
            if (string.IsNullOrWhiteSpace(code))
            {
                return View("Error");
            }

            var message = "Your security code is: " + code;
            if (model.SelectedProvider == "Email")
            {
                await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message);
            }
            else if (model.SelectedProvider == "Phone")
            {
                await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message);
            }

            return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
        }

        //
        // GET: /Account/VerifyCode
        [HttpGet]
        [AllowAnonymous]
        public async Task<IActionResult> VerifyCode(string provider, bool rememberMe, string returnUrl = null)
        {
            // Require that the user has already logged in via username/password or external login
            var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (user == null)
            {
                return View("Error");
            }
            return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe });
        }

        //
        // POST: /Account/VerifyCode
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> VerifyCode(VerifyCodeViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // The following code protects for brute force attacks against the two factor codes.
            // If a user enters incorrect codes for a specified amount of time then the user account
            // will be locked out for a specified amount of time.
            var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser);
            if (result.Succeeded)
            {
                return RedirectToLocal(model.ReturnUrl);
            }
            if (result.IsLockedOut)
            {
                _logger.LogWarning(7, "User account locked out.");
                return View("Lockout");
            }
            else
            {
                ModelState.AddModelError(string.Empty, "Invalid code.");
                return View(model);
            }
        }

        //
        // GET /Account/AccessDenied
        [HttpGet]
        public IActionResult AccessDenied()
        {
            return View();
        }

        #region Helpers

        private void AddErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }
        }

        private IActionResult RedirectToLocal(string returnUrl)
        {
            if (Url.IsLocalUrl(returnUrl))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction(nameof(HomeController.Index), "Home");
            }
        }

        #endregion
    }

Step Eight – Adding the Log-in Button

The next step is to change the master page, this is typically Layout.cshtml. Here, we just add a reference to another file (_LoginPartial):

            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
                    <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
                    <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
                </ul>
                @await Html.PartialAsync("_LoginPartial")
            </div>
        </div>
    </nav>
    <div class="container body-content">
        @RenderBody()

LoginPartial looks like this:

@using Microsoft.AspNetCore.Identity
@using MyWebApp.Models

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

@if (SignInManager.IsSignedIn(User))
{
    <form asp-area="" asp-controller="Account" asp-action="Logout" method="post" id="logoutForm" class="navbar-right">
        <ul class="nav navbar-nav navbar-right">
            <li>
                <a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage">Hello @UserManager.GetUserName(User)!</a>
            </li>
            <li>
                <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button>
            </li>
        </ul>
    </form>
}
else
{
    <ul class="nav navbar-nav navbar-right">
        <li><a asp-area="" asp-controller="Account" asp-action="Register">Register</a></li>
        <li><a asp-area="" asp-controller="Account" asp-action="Login">Log in</a></li>
    </ul>
}

… and that’s it. When you’re done, your website should provide basic log-in and register functionality; the following section has some suggestions about what to do if it does not.

Errors

The following are errors you may encounter at this stage, depending on what state your project was in before you started this.

DbContext Error

An unhandled exception occurred while processing the request.
InvalidOperationException: Unable to resolve service for type ‘MyWebApp.Data.ApplicationDbContext’ while attempting to activate ‘Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`4[MyWebApp.Models.ApplicationUser,Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole,MyWebApp.Data.ApplicationDbContext,System.String]’.
Microsoft.Extensions.DependencyInjection.ServiceLookup.Service.PopulateCallSites(ServiceProvider provider, ISet callSiteChain, ParameterInfo[] parameters, bool throwIfCallSiteNotFound)

This is simply because the DbContext was never registered; the fix is:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            // Add framework services.
            services.AddMvc();
        }

You’ll need the following NuGet package installed:

Microsoft.EntityFrameworkCore.SqlServer

And you’ll need to add:

using Microsoft.EntityFrameworkCore;

ConnectionString Error

An unhandled exception occurred while processing the request.
ArgumentNullException: Value cannot be null.
Parameter name: connectionString
Microsoft.EntityFrameworkCore.Utilities.Check.NotEmpty(string value, string parameterName)

Admittedly, it’s not rocket science to work this one out; your appsettings.json needs a connection string. By default, this uses SQLExpress, but you can actually point it to any DB:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=ServerName\\InstanceName;Database=MyDatabase;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

Identity.External Error

An unhandled exception occurred while processing the request.
InvalidOperationException: No authentication handler is configured to handle the scheme: Identity.External
Microsoft.AspNetCore.Http.Authentication.Internal.DefaultAuthenticationManager+d__15.MoveNext()

In Startup.cs, change the Configure function to include the following:

    . . .
    app.UseStaticFiles();

    app.UseIdentity();

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

Error in Compilation of Required Resource

An error occurred during the compilation of a resource required to process this request. Please review the following specific error details and modify your source code appropriately.

Check the _ViewImports.cshtml – this is where all the using statements for the views are held; it should include all the necessary namespaces; for example:

@using MyApp.Web.Core
@using MyApp
@using MyApp.Web.Core.Models
@using MyApp.Web.Core.Models.AccountViewModels
@using MyApp.Web.Core.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Could not find Table AspNetUsers

In Visual Studio, you can use the Package Manager Console to apply pending migrations to the database:

PM> Update-Database

Alternatively, you can apply pending migrations from a command prompt at your project directory:

> dotnet ef database update

To set-up a migration, you need the package:

Microsoft.EntityFrameworkCore.Design

Set-up a migration:

Add an identity migration (this is the default one):

    public partial class CreateIdentitySchema : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "AspNetRoles",
                columns: table => new
                {
                    Id = table.Column<string>(nullable: false),
                    ConcurrencyStamp = table.Column<string>(nullable: true),
                    Name = table.Column<string>(maxLength: 256, nullable: true),
                    NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetRoles", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "AspNetUserTokens",
                columns: table => new
                {
                    UserId = table.Column<string>(nullable: false),
                    LoginProvider = table.Column<string>(nullable: false),
                    Name = table.Column<string>(nullable: false),
                    Value = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
                });

            migrationBuilder.CreateTable(
                name: "AspNetUsers",
                columns: table => new
                {
                    Id = table.Column<string>(nullable: false),
                    AccessFailedCount = table.Column<int>(nullable: false),
                    ConcurrencyStamp = table.Column<string>(nullable: true),
                    Email = table.Column<string>(maxLength: 256, nullable: true),
                    EmailConfirmed = table.Column<bool>(nullable: false),
                    LockoutEnabled = table.Column<bool>(nullable: false),
                    LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
                    NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
                    NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
                    PasswordHash = table.Column<string>(nullable: true),
                    PhoneNumber = table.Column<string>(nullable: true),
                    PhoneNumberConfirmed = table.Column<bool>(nullable: false),
                    SecurityStamp = table.Column<string>(nullable: true),
                    TwoFactorEnabled = table.Column<bool>(nullable: false),
                    UserName = table.Column<string>(maxLength: 256, nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUsers", x => x.Id);
                });

            migrationBuilder.CreateTable(
                name: "AspNetRoleClaims",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    RoleId = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
                    table.ForeignKey(
                        name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
                        column: x => x.RoleId,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "AspNetUserClaims",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    UserId = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
                    table.ForeignKey(
                        name: "FK_AspNetUserClaims_AspNetUsers_UserId",
                        column: x => x.UserId,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "AspNetUserLogins",
                columns: table => new
                {
                    LoginProvider = table.Column<string>(nullable: false),
                    ProviderKey = table.Column<string>(nullable: false),
                    ProviderDisplayName = table.Column<string>(nullable: true),
                    UserId = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
                    table.ForeignKey(
                        name: "FK_AspNetUserLogins_AspNetUsers_UserId",
                        column: x => x.UserId,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateTable(
                name: "AspNetUserRoles",
                columns: table => new
                {
                    UserId = table.Column<string>(nullable: false),
                    RoleId = table.Column<string>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
                        column: x => x.RoleId,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetUsers_UserId",
                        column: x => x.UserId,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "RoleNameIndex",
                table: "AspNetRoles",
                column: "NormalizedName");

            migrationBuilder.CreateIndex(
                name: "IX_AspNetRoleClaims_RoleId",
                table: "AspNetRoleClaims",
                column: "RoleId");

            migrationBuilder.CreateIndex(
                name: "IX_AspNetUserClaims_UserId",
                table: "AspNetUserClaims",
                column: "UserId");

            migrationBuilder.CreateIndex(
                name: "IX_AspNetUserLogins_UserId",
                table: "AspNetUserLogins",
                column: "UserId");

            migrationBuilder.CreateIndex(
                name: "IX_AspNetUserRoles_RoleId",
                table: "AspNetUserRoles",
                column: "RoleId");

            migrationBuilder.CreateIndex(
                name: "IX_AspNetUserRoles_UserId",
                table: "AspNetUserRoles",
                column: "UserId");

            migrationBuilder.CreateIndex(
                name: "EmailIndex",
                table: "AspNetUsers",
                column: "NormalizedEmail");

            migrationBuilder.CreateIndex(
                name: "UserNameIndex",
                table: "AspNetUsers",
                column: "NormalizedUserName",
                unique: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "AspNetRoleClaims");

            migrationBuilder.DropTable(
                name: "AspNetUserClaims");

            migrationBuilder.DropTable(
                name: "AspNetUserLogins");

            migrationBuilder.DropTable(
                name: "AspNetUserRoles");

            migrationBuilder.DropTable(
                name: "AspNetUserTokens");

            migrationBuilder.DropTable(
                name: "AspNetRoles");

            migrationBuilder.DropTable(
                name: "AspNetUsers");
        }
    }

You’ll need the .designer.cs file, too:

Now, if you run:

 Update-Database

Or, run

dotnet ef database update

From powershell (project directory); it should update your database:

Disclaimer

Just to point out the obvious here: I didn’t create this identity system, I simply took what was supplied by default, and applied it to an existing project. The code above is not mine – it’s all copied and pasted by simply creating a new project with Identity Services and copying the relevant parts.

References

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity

Using Entity Framework Core with DBFirst

Say what you like about ORM frameworks, but they do decrease time to market. My impression, as someone that has generally had very little exposure to them, is that, whilst they can make it very quick to get something up and running, they make it very easy to shoot yourself in the foot.

With all the hype about .Net Core, I thought I’d give EF Core a go, and this post is a document of my initial set-up which was, by no means, a straight forward process!

The General Idea of an ORM

The purpose of an ORM is to abstract data access, in a manner similar to the following:

The great advantage of this is that you can very quickly make database changes and maintain a layer of abstraction between the DB and the accessing layer. Obviously, the downside is that you don’t have the same level of control over this access.

Pre-requisites

The first step is to install the SDK from here. Like everything else in this post, this web-site is not as straight-forward as it appears. Make sure that you select “Current” and “SDK”:

I used x64. That matters when you get further down.

Database First

In this particular installation, I’m using the “Database First” model. What that means is that I already have a database, and it is sat on an accessible machine. The following project will create classes to access that. For details of how to create a database using a VS project, see this article that I wrote on unit testing databases.

Project set-up

The first step is to create your project.

What is the difference between ASP.NET Core (.NET Core) and ASP.NET Core (.NET Framework)?

In this instance, we’ll go with .Net Core. The difference between the two is that one of them (.NET Framework) references the .NET Framework, and so will not be cross platform. Obviously, picking .NET Core is your smallest footprint, and least functionality.

Pick Web API here, as we’re essentially just writing a service that accesses a DB (see the diagram above).

The next step is to install Entity Framework Core:

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore/

A note on project.json

This is a new file that has been introduced into the .Net Core world, and it is (IMHO) a huge improvement on the flakey confusion of NuGet package management. The idea is that you declare your dependencies, in a similar way that you might declare your variables in a program. When you change this, or when you ask it to, VS will simply go and get these packages for you. This means, for example, that I can paste my project.json in this post, and you, the reader (or future me), can simply paste this directly into yours and VS will do the rest… so:

{
  "dependencies": {
    "Microsoft.ApplicationInsights.AspNetCore": "2.0.0",
    "Microsoft.AspNetCore.Hosting.Abstractions": "1.1.0",
    "Microsoft.AspNetCore.Mvc": "1.1.1",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.EntityFrameworkCore": "1.1.0",
    "Microsoft.EntityFrameworkCore.Design": "1.1.0",
    "Microsoft.EntityFrameworkCore.Relational.Design": "1.1.0",
    "Microsoft.EntityFrameworkCore.SqlServer": "1.1.0",
    "Microsoft.EntityFrameworkCore.Tools.DotNet": "1.1.0-preview4-final",
    "Microsoft.Extensions.Configuration.Json": "1.1.0",
    "Microsoft.Extensions.Logging": "1.1.0",
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.Extensions.Logging.Debug": "1.1.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "NETStandard.Library": "1.6.1",
    "System.Collections.NonGeneric": "4.3.0",
 
    "Microsoft.EntityFrameworkCore.SqlServer.Design": {
      "version": "1.1.0",
      "type": "build"
    },
    "Microsoft.EntityFrameworkCore.Tools": {
      "version": "1.1.0-preview4-final",
      "type": "build"
    }
  },
 
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final",
    "Microsoft.EntityFrameworkCore.Tools": "1.1.0-preview4-final"
  },
 
  "frameworks": {
    "netcoreapp1.1": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
 
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
 
  "runtimes": {
    "win10-x64": {}
  },
 
  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config"
    ]
  },
 
  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  }
}

If you now simply open a package manager window in VS and type:

dotnet restore

VS should do the rest.

The next thing that you’ll need to update is the appsettings.json:

{
 
  "ConnectionStrings": {
    "JourneyDB": "Data Source=ServerName\\DatabaseInstance; Initial Catalog=MyDatabase; Integrated Security=SSPI"
  },
  
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Scaffolding

So, you should now have a project that’s ready to go. I suggestion, unless you’re reading this in around a year from now (2018) when all this has been stabilised, is that you restart VS. In fact, this should be your first response if anything in this post doesn’t do what you expect (it is still in preview, so I’m not judging).

The next step is to call the following command:

Scaffold-DbContext "Server=ServerName\DatabaseInstance;Database=MyDatabase;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

What should that do (because it doesn’t)?

Okay, it took me a good while to get this to actually work, so the chance of it working first time is pretty remote; but when it does work, you should get a mirror of your DB in the project:

Using it

As you can see, I have a `JourneyHeader`; here’s the code to get the contents of that table:

namespace JourneyService.Controllers
{
    [Route("api/[controller]")]
    public class JourneyController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<JourneyHeader> Get()
        {
            using (JourneyDatabaseContext context = new JourneyDatabaseContext())
            {
                return context.JourneyHeader.ToList();
            }            
        }

And here’s the proof that it works:

References

https://code.msdn.microsoft.com/How-to-using-Entity-1464feea

http://www.dotnetspeak.com/asp-net-core/write-your-first-api-with-asp-net-core-and-entity-framework-core/

https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db

http://michaelcrump.net/getting-started-with-aspnetcore/

https://github.com/dotnet/core/blob/master/release-notes/rc4-download.md

https://blogs.msdn.microsoft.com/dotnet/2016/11/16/announcing-entity-framework-core-1-1/

http://stackoverflow.com/questions/42393977/setting-up-database-first-ef-project-using-scaffold-dbcontext

http://stackoverflow.com/questions/42393977/setting-up-database-first-ef-project-using-scaffold-dbcontext?noredirect=1#comment71956574_42393977