Create an ASP.NET Core app with user data protected by authorization

By Rick Anderson and Joe Audette

This tutorial shows how to create a web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups:

  • Registered users can view all the approved contact data.
  • Registered users can edit/delete their own data.
  • Managers can approve or reject contact data. Only approved contacts are visible to users.
  • Administrators can approve/reject and edit/delete any data.

In the following image, user Rick (rick@example.com) is signed in. User Rick can only view approved contacts and edit/delete his contacts. Only the last record, created by Rick, displays edit and delete links

image described above

In the following image, manager@contoso.com is signed in and in the managers role.

image described above

The following image shows the managers details view of a contact.

image described above

Only managers and administrators have the approve and reject buttons.

In the following image, admin@contoso.com is signed in and in the administrator’s role.

image described above

The administrator has all privileges. She can read/edit/delete any contact and change the status of contacts.

The app was created by scaffolding the following Contact model:

public class Contact
{
    public int ContactId { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    public string Email { get; set; }
}

A ContactIsOwnerAuthorizationHandler authorization handler ensures that a user can only edit their data. A ContactManagerAuthorizationHandler authorization handler allows managers to approve or reject contacts. A ContactAdministratorsAuthorizationHandler authorization handler allows administrators to approve or reject contacts and to edit/delete contacts.

Prerequisites

This is not a beginning tutorial. You should be familiar with:

The starter and completed app

Download the completed app. Test the completed app so you become familiar with its security features.

The starter app

It's helpful to compare your code with the completed sample.

Download the starter app.

See Create the starter app if you'd like to create it from scratch.

Update the database:

   dotnet ef database update

Run the app, tap the ContactManager link, and verify you can create, edit, and delete a contact.

This tutorial has all the major steps to create the secure user data app. You may find it helpful to refer to the completed project.

Modify the app to secure user data

The following sections have all the major steps to create the secure user data app. You may find it helpful to refer to the completed project.

Tie the contact data to the user

Use the ASP.NET Identity user ID to ensure users can edit their data, but not other users data. Add OwnerID to the Contact model:

public class Contact
{
    public int ContactId { get; set; }

    // user ID from AspNetUser table
    public string OwnerID { get; set; }

    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public ContactStatus Status { get; set; }
}

public enum ContactStatus
{
    Submitted,
    Approved,
    Rejected
}

OwnerID is the user's ID from the AspNetUser table in the Identity database. The Status field determines if a contact is viewable by general users.

Scaffold a new migration and update the database:

dotnet ef migrations add userID_Status
dotnet ef database update

Require SSL and authenticated users

In the ConfigureServices method of the Startup.cs file, add the RequireHttpsAttribute authorization filter:

var skipSSL = Configuration.GetValue<bool>("LocalTest:skipSSL");
// requires using Microsoft.AspNetCore.Mvc;
services.Configure<MvcOptions>(options =>
{
// Set LocalTest:skipSSL to true to skip SSL requrement in 
// debug mode. This is useful when not using Visual Studio.
if (_hostingEnv.IsDevelopment() && !skipSSL)
    {
        options.Filters.Add(new RequireHttpsAttribute());
    }
});

If you're using Visual Studio, see Set up IIS Express for SSL/HTTPS. To redirect HTTP requests to HTTPS, see URL Rewriting Middleware. If you are using Visual Studio Code or testing on local platform that doesn't include a test certificate for SSL:

  • Set "LocalTest:skipSSL": true in the appsettings.json file.

Require authenticated users

Set the default authentication policy to require users to be authenticated. You can opt out of authentication at the controller or action method with the [AllowAnonymous] attribute. With this approach, any new controllers added will automatically require authentication, which is safer than relying on new controllers to include the [Authorize] attribute. Add the following to the ConfigureServices method of the Startup.cs file:

// requires: using Microsoft.AspNetCore.Authorization;
//           using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

Add [AllowAnonymous] to the home controller so anonymous users can get information about the site before they register.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace ContactManager.Controllers
{
    [AllowAnonymous]
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

Configure the test account

The SeedData class creates two accounts, administrator and manager. Use the Secret Manager tool to set a password for these accounts. Do this from the project directory (the directory containing Program.cs).

dotnet user-secrets set SeedUserPW <PW>

Update Configure to use the test password:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseIdentity();

    app.UseMvcWithDefaultRoute();

    // Set password with the Secret Manager tool.
    // dotnet user-secrets set SeedUserPW <pw>
    var testUserPw = Configuration["SeedUserPW"];

    if (String.IsNullOrEmpty(testUserPw))
    {
        throw new System.Exception("Use secrets manager to set SeedUserPW \n" +
                                   "dotnet user-secrets set SeedUserPW <pw>");
    }

    try
    {
        SeedData.Initialize(app.ApplicationServices, testUserPw).Wait();
    }
    catch
    {
        throw new System.Exception("You need to update the DB "
            + "\nPM > Update-Database " + "\n or \n" +
              "> dotnet ef database update"
              + "\nIf that doesn't work, comment out SeedData and "
              + "register a new user");
    }

Add the administrator user ID and Status = ContactStatus.Approved to the contacts. Only one contact is shown, add the user ID to all contacts:

public static void SeedDB(ApplicationDbContext context, string adminID)
{
    if (context.Contact.Any())
    {
        return;   // DB has been seeded
    }

    context.Contact.AddRange(
        new Contact
        {
            Name = "Debra Garcia",
            Address = "1234 Main St",
            City = "Redmond",
            State = "WA",
            Zip = "10999",
            Email = "debra@example.com",
            Status = ContactStatus.Approved,
            OwnerID = adminID
        },

Create owner, manager, and administrator authorization handlers

Create a ContactIsOwnerAuthorizationHandler class in the Authorization folder. The ContactIsOwnerAuthorizationHandler will verify the user acting on the resource owns the resource.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactIsOwnerAuthorizationHandler
                : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        UserManager<ApplicationUser> _userManager;

        public ContactIsOwnerAuthorizationHandler(UserManager<ApplicationUser> 
            userManager)
        {
            _userManager = userManager;
        }

        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.FromResult(0);
            }

            // If we're not asking for CRUD permission, return.

            if (requirement.Name != Constants.CreateOperationName &&
                requirement.Name != Constants.ReadOperationName   &&
                requirement.Name != Constants.UpdateOperationName &&
                requirement.Name != Constants.DeleteOperationName )
            {
                return Task.FromResult(0);
            }

            if (resource.OwnerID == _userManager.GetUserId(context.User))
            {
                context.Succeed(requirement);
            }

            return Task.FromResult(0);
        }
    }
}

The ContactIsOwnerAuthorizationHandler calls context.Succeed if the current authenticated user is the contact owner. Authorization handlers generally return context.Succeed when the requirements are met. They return Task.FromResult(0) when requirements are not met. Task.FromResult(0) is neither success or failure, it allows other authorization handler to run. If you need to explicitly fail, return context.Fail().

We allow contact owners to edit/delete their own data, so we don't need to check the operation passed in the requirement parameter.

Create a manager authorization handler

Create a ContactManagerAuthorizationHandler class in the Authorization folder. The ContactManagerAuthorizationHandler will verify the user acting on the resource is a manager. Only managers can approve or reject content changes (new or changed).

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Identity;

namespace ContactManager.Authorization
{
    public class ContactManagerAuthorizationHandler :
        AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task
            HandleRequirementAsync(AuthorizationHandlerContext context,
                                   OperationAuthorizationRequirement requirement,
                                   Contact resource)
        {
            if (context.User == null || resource == null)
            {
                return Task.FromResult(0);
            }

            // If not asking for approval/reject, return.
            if (requirement.Name != Constants.ApproveOperationName &&
                requirement.Name != Constants.RejectOperationName)
            {
                return Task.FromResult(0);
            }

            // Managers can approve or reject.
            if (context.User.IsInRole(Constants.ContactManagersRole))
            {
                context.Succeed(requirement);
            }

            return Task.FromResult(0);
        }
    }
}

Create an administrator authorization handler

Create a ContactAdministratorsAuthorizationHandler class in the Authorization folder. The ContactAdministratorsAuthorizationHandler will verify the user acting on the resource is a administrator. Administrator can do all operations.

using System.Threading.Tasks;
using ContactManager.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public class ContactAdministratorsAuthorizationHandler
                    : AuthorizationHandler<OperationAuthorizationRequirement, Contact>
    {
        protected override Task HandleRequirementAsync(
                                              AuthorizationHandlerContext context,
                                    OperationAuthorizationRequirement requirement, 
                                     Contact resource)
        {
            if (context.User == null)
            {
                return Task.FromResult(0);
            }

            // Administrators can do anything.
            if (context.User.IsInRole(Constants.ContactAdministratorsRole))
            {
                context.Succeed(requirement);
            }

            return Task.FromResult(0);
        }
    }
}

Register the authorization handlers

Services using Entity Framework Core must be registered for dependency injection using AddScoped. The ContactIsOwnerAuthorizationHandler uses ASP.NET Core Identity, which is built on Entity Framework Core. Register the handlers with the service collection so they will be available to the ContactsController through dependency injection. Add the following code to the end of ConfigureServices:

// Authorization handlers.
services.AddScoped<IAuthorizationHandler,
                      ContactIsOwnerAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
                      ContactAdministratorsAuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler,
                      ContactManagerAuthorizationHandler>();

ContactAdministratorsAuthorizationHandler and ContactManagerAuthorizationHandler are added as singletons. They are singletons because they don't use EF and all the information needed is in the Context parameter of the HandleRequirementAsync method.

The complete ConfigureServices:

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

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

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();

    var skipSSL = Configuration.GetValue<bool>("LocalTest:skipSSL");
    // requires using Microsoft.AspNetCore.Mvc;
    services.Configure<MvcOptions>(options =>
    {
    // Set LocalTest:skipSSL to true to skip SSL requrement in 
    // debug mode. This is useful when not using Visual Studio.
    if (_hostingEnv.IsDevelopment() && !skipSSL)
        {
            options.Filters.Add(new RequireHttpsAttribute());
        }
    });

    // requires: using Microsoft.AspNetCore.Authorization;
    //           using Microsoft.AspNetCore.Mvc.Authorization;
    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

    // Authorization handlers.
    services.AddScoped<IAuthorizationHandler,
                          ContactIsOwnerAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactAdministratorsAuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler,
                          ContactManagerAuthorizationHandler>();
}

Update the code to support authorization

In this section, you update the controller and views and add an operations requirements class.

Update the Contacts controller

Update the ContactsController constructor:

  • Add the IAuthorizationService service to access to the authorization handlers.
  • Add the Identity UserManager service:
public class ContactsController : Controller
{
    private readonly ApplicationDbContext _context;
    private readonly IAuthorizationService _authorizationService;
    private readonly UserManager<ApplicationUser> _userManager;

    public ContactsController(
        ApplicationDbContext context,
        IAuthorizationService authorizationService,
        UserManager<ApplicationUser> userManager)
    {
        _context = context;
        _userManager = userManager;
        _authorizationService = authorizationService;
    }

Add a contact operations requirements class

Add the ContactOperationsRequirements class to the Authorization folder. This class contain the requirements our app supports:

using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Update Create

Update the HTTP POST Create method to:

  • Add the user ID to the Contact model.
  • Call the authorization handler to verify the user owns the contact.
// POST: Contacts/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(ContactEditViewModel editModel)
{
    if (!ModelState.IsValid)
    {
        return View(editModel);
    }

    var contact = ViewModel_to_model(new Contact(), editModel);

    contact.OwnerID = _userManager.GetUserId(User);

    var isAuthorized = await _authorizationService.AuthorizeAsync(
                                                User, contact,
                                                ContactOperations.Create);
    if (!isAuthorized)
    {
        return new ChallengeResult();
    }

    _context.Add(contact);
    await _context.SaveChangesAsync();
    return RedirectToAction("Index");
}

Update Edit

Update both Edit methods to use the authorization handler to verify the user owns the contact. Because we are performing resource authorization we cannot use the [Authorize] attribute. We don't have access to the resource when attributes are evaluated. Resource based authorization must be imperative. Checks must be performed once we have access to the resource, either by loading it in our controller, or by loading it within the handler itself. Frequently you will access the resource by passing in the resource key.

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var contact = await _context.Contact.SingleOrDefaultAsync(
                                                m => m.ContactId == id);
    if (contact == null)
    {
        return NotFound();
    }

    var isAuthorized = await _authorizationService.AuthorizeAsync(
                                                User, contact,
                                                ContactOperations.Update);
    if (!isAuthorized)
    {
        return new ChallengeResult();
    }

    var editModel = Model_to_viewModel(contact);

    return View(editModel);
}

// POST: Contacts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, ContactEditViewModel editModel)
{
    if (!ModelState.IsValid)
    {
        return View(editModel);
    }

    // Fetch Contact from DB to get OwnerID.
    var contact = await _context.Contact.SingleOrDefaultAsync(m => m.ContactId == id);
    if (contact == null)
    {
        return NotFound();
    }

    var isAuthorized = await _authorizationService.AuthorizeAsync(User, contact,
                                                        ContactOperations.Update);
    if (!isAuthorized)
    {
        return new ChallengeResult();
    }

    contact = ViewModel_to_model(contact, editModel);

    if (contact.Status == ContactStatus.Approved)
    {
        // If the contact is updated after approval, 
        // and the user cannot approve set the status back to submitted
        var canApprove = await _authorizationService.AuthorizeAsync(User, contact,
                                ContactOperations.Approve);

        if (!canApprove) contact.Status = ContactStatus.Submitted;
    }

    _context.Update(contact);
    await _context.SaveChangesAsync();

    return RedirectToAction("Index");
}

Update the Delete method

Update both Delete methods to use the authorization handler to verify the user owns the contact.

public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var contact = await _context.Contact.SingleOrDefaultAsync(m => m.ContactId == id);
    if (contact == null)
    {
        return NotFound();
    }

    var isAuthorized = await _authorizationService.AuthorizeAsync(User, contact,
                                ContactOperations.Delete);
    if (!isAuthorized)
    {
        return new ChallengeResult();
    }

    return View(contact);
}

// POST: Contacts/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var contact = await _context.Contact.SingleOrDefaultAsync(m => m.ContactId == id);

    var isAuthorized = await _authorizationService.AuthorizeAsync(User, contact,
                                ContactOperations.Delete);
    if (!isAuthorized)
    {
        return new ChallengeResult();
    }

    _context.Contact.Remove(contact);
    await _context.SaveChangesAsync();
    return RedirectToAction("Index");
}

Inject the authorization service into the views

Currently the UI shows edit and delete links for data the user cannot modify. We'll fix that by applying the authorization handler to the views.

Inject the authorization service in the Views/_ViewImports.cshtml file so it will be available to all views:

@using ContactManager
@using ContactManager.Models
@using ContactManager.Models.AccountViewModels
@using ContactManager.Models.ManageViewModels
@using ContactManager.Authorization
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService 

Update the Views/Contacts/Index.cshtml Razor view to only display the edit and delete links for users who can edit/delete the contact.

Add @using ContactManager.Authorization;

Update the Edit and Delete links so they are only rendered for users with permission to edit and delete the contact.

    </td>
    <td>
        @Html.DisplayFor(modelItem => item.Zip)
    </td>
    <td>
        @Html.DisplayFor(modelItem => item.Status)
    </td>
    <td>
        @if (await AuthorizationService.AuthorizeAsync(User,
                                           item, ContactOperations.Update))
        {
            <a asp-action="Edit" asp-route-id="@item.ContactId">Edit</a><text> | </text>
        }
        <a asp-action="Details" asp-route-id="@item.ContactId">Details</a>
        @if (await AuthorizationService.AuthorizeAsync(User,
                                             item, ContactOperations.Delete))
        {
            <text> | </text>
            <a asp-action="Delete" asp-route-id="@item.ContactId">Delete</a>
        }
    </td>
</tr>

Warning: Hiding links from users that do not have permission to edit or delete data does not secure the app. Hiding links makes the app more user friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data they don't own. The controller must repeat the access checks to be secure.

Update the Details view

Update the details view so managers can approve or reject contacts:

        <dt>
            @Html.DisplayNameFor(model => model.Zip)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Zip)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Status)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Status)
        </dd>
    </dl>
</div>
@if (Model.Status != ContactStatus.Approved)
{
    @if (await AuthorizationService.AuthorizeAsync(User, Model, ContactOperations.Approve))
    {
        <form asp-action="SetStatus" asp-controller="Contacts" style="display:inline;">
            <input type="hidden" name="id" value="@Model.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Approved" />
            <button type="submit" class="btn btn-xs btn-success">Approve</button>
        </form>
    }  
}
@if (Model.Status != ContactStatus.Rejected)
{
    @if (await AuthorizationService.AuthorizeAsync(User, Model, ContactOperations.Reject))
    {
        <form asp-action="SetStatus" asp-controller="Contacts" style="display:inline;">
            <input type="hidden" name="id" value="@Model.ContactId" />
            <input type="hidden" name="status" value="@ContactStatus.Rejected" />
            <button type="submit" class="btn btn-xs btn-danger">Reject</button>
        </form>
    }
}
<div>
@* Uncomment to perform authorization check. A real app would hide the edit link from users
        uses who don't have edit access. A user without edit access can click the link but will get denied 
        access in the controller.  
    @if(await AuthorizationService.AuthorizeAsync(User, Model, ContactOperations.Update))
    {
*@
        <a asp-action="Edit" asp-route-id="@Model.ContactId">Edit</a> <text>|</text>
@*
    }
*@
    <a asp-action="Index">Back to List</a>
</div>

Test the completed app

If you are using Visual Studio Code or testing on local platform that doesn't include a test certificate for SSL:

  • Set "LocalTest:skipSSL": true in the appsettings.json file.

If you have run the app and have contacts, delete all the records in the Contact table and restart the app to seed the database. If you are using Visual Studio, you need to exit and restart IIS Express to seed the database.

Register a user to browse the contacts.

An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate versions). In one browser, register a new user, for example, test@example.com. Sign in to each browser with a different user. Verify the following:

  • Registered users can view all the approved contact data.
  • Registered users can edit/delete their own data.
  • Managers can approve or reject contact data. The Details view shows Approve and Reject buttons.
  • Administrators can approve/reject and edit/delete any data.
User Options
test@example.com Can edit/delete own data
manager@contoso.com Can approve/reject and edit/delete own data
admin@contoso.com Can edit/delete and approve/reject all data

Create a contact in the administrators browser. Copy the URL for delete and edit from the administrator contact. Paste these links into the test user's browser to verify the test user cannot perform these operations.

Create the starter app

Follow these instructions to create the starter app.

  • Create an ASP.NET Core Web Application using Visual Studio 2017 named "ContactManager"

    • Create the app with Individual User Accounts.
    • Name it "ContactManager" so your namespace will match the namespace use in the sample.
  • Add the following Contact model:

    public class Contact
    {
        public int ContactId { get; set; }
    
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
        public string Email { get; set; }
    }
    
  • Scaffold the Contact model using Entity Framework Core and the ApplicationDbContext data context. Accept all the scaffolding defaults. Using ApplicationDbContext for the data context class puts the contact table in the Identity database. See Adding a model for more information.

  • Update the ContactManager anchor in the Views/Shared/_Layout.cshtml file from asp-controller="Home" to asp-controller="Contacts" so tapping the ContactManager link will invoke the Contacts controller. The original markup:

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

The updated markup:

   <a asp-area="" asp-controller="Contacts" asp-action="Index" class="navbar-brand">ContactManager</a>
  • Scaffold the initial migration and update the database
   dotnet ef migrations add initial
   dotnet ef database update
  • Test the app by creating, editing and deleting a contact

Seed the database

Add the SeedData class to the Data folder. If you've downloaded the sample, you can copy the SeedData.cs file to the Data folder of the starter project.


using ContactManager.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContactManager.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, string testUserPw)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {
                var uid = await CreateTestUser(serviceProvider, testUserPw);
                SeedDB(context, uid);
            }
        }

        private static async Task<string> CreateTestUser(IServiceProvider serviceProvider, string testUserPw)
        {
            if (String.IsNullOrEmpty(testUserPw))
                return "";

            const string SeedUserName = "test@example.com";

            var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();

            var user = await userManager.FindByNameAsync(SeedUserName);
            if (user == null)
            {
                user = new ApplicationUser { UserName = SeedUserName };
                await userManager.CreateAsync(user, testUserPw);
            }

            return user.Id;
        }

        public static void SeedDB(ApplicationDbContext context, string uid)
        {
            if (context.Contact.Any())
            {
                return;   // DB has been seeded
            }

            context.Contact.AddRange(
                new Contact
                {
                    Name = "Debra Garcia",
                    Address = "1234 Main St",
                    City = "Redmond",
                    State = "WA",
                    Zip = "10999",
                    Email = "debra@example.com"
                },
             new Contact
             {
                 Name = "Thorsten Weinrich",
                 Address = "5678 1st Ave W",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "thorsten@example.com"
             },
             new Contact
             {
                 Name = "Yuhong Li",
                 Address = "9012 State st",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "yuhong@example.com"
             },
             new Contact
             {
                 Name = "Jon Orton",
                 Address = "3456 Maple St",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "jon@example.com"
             },
             new Contact
             {
                 Name = "Diliana Alexieva-Bosseva",
                 Address = "7890 2nd Ave E",
                 City = "Redmond",
                 State = "WA",
                 Zip = "10999",
                 Email = "diliana@example.com"
             }
             );
            context.SaveChanges();
        }
    }
}

Add the highlighted code to the end of the Configure method in the Startup.cs file:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseIdentity();

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

    try
    {
        SeedData.Initialize(app.ApplicationServices, "").Wait();
    }
    catch
    {
        throw new System.Exception("You need to update the DB "
            + "\nPM > Update-Database " + "\n or \n" +
              "> dotnet ef database update"
              + "\nIf that doesn't work, comment out SeedData and register a new user");
    }
}

Test that the app seeded the database. The seed method does not run if there are any rows in the contact DB.

Create a class used in the tutorial

  • Create a folder named Authorization.
  • Copy the Authorization\ContactOperations.cs file from the completed project download, or copy the following code:
using Microsoft.AspNetCore.Authorization.Infrastructure;

namespace ContactManager.Authorization
{
    public static class ContactOperations
    {
        public static OperationAuthorizationRequirement Create =   
          new OperationAuthorizationRequirement {Name=Constants.CreateOperationName};
        public static OperationAuthorizationRequirement Read = 
          new OperationAuthorizationRequirement {Name=Constants.ReadOperationName};  
        public static OperationAuthorizationRequirement Update = 
          new OperationAuthorizationRequirement {Name=Constants.UpdateOperationName}; 
        public static OperationAuthorizationRequirement Delete = 
          new OperationAuthorizationRequirement {Name=Constants.DeleteOperationName};
        public static OperationAuthorizationRequirement Approve = 
          new OperationAuthorizationRequirement {Name=Constants.ApproveOperationName};
        public static OperationAuthorizationRequirement Reject = 
          new OperationAuthorizationRequirement {Name=Constants.RejectOperationName};
    }

    public class Constants
    {
        public static readonly string CreateOperationName = "Create";
        public static readonly string ReadOperationName = "Read";
        public static readonly string UpdateOperationName = "Update";
        public static readonly string DeleteOperationName = "Delete";
        public static readonly string ApproveOperationName = "Approve";
        public static readonly string RejectOperationName = "Reject";

        public static readonly string ContactAdministratorsRole = 
                                                              "ContactAdministrators";
        public static readonly string ContactManagersRole = "ContactManagers";
    }
}

Additional resources