web

.NET

.NET

In this quickstart, you are going to build an application with .NET 7 and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-dotnet-web.

Prerequisites

  • .NET: This is the framework needed to run .NET applications. This app has been tested with .NET 7.0.7. This example should work with future compatible versions of .NET.
  • Docker: The quickest way to stand up FusionAuth. (There are other ways).
  • Visual Studio Code: The editor for making changes to code. (You can use other editors as well.)

General Architecture

While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

In this section, you’ll get FusionAuth up and running and use .NET to create a new application.

Clone the Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-dotnet-web.git
cd fusionauth-quickstart-dotnet-web

Run FusionAuth via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is e9fdb985-9173-4e01-9d73-ac2d60d1dc8e.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example username is richard@example.com and the password is password.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Create your .NET 7 Application

Now you are going to create a .NET application using C#. While this section builds a simple .NET web application, you can use the same configuration to integrate your existing .NET application with FusionAuth.

You’ll use Razor Pages and .NET 7. This application will display common information to all users. There will also be a secured area, only available to an authenticated user. The full source code is available if you want to download it and take a look.

If you simply want to run the application , there is a completed version in the ‘complete-application’ directory. You can use the following commands to get it up and running if you do not want to create your own.

cd complete-application
dotnet run

Then view the website at the following location http://localhost:5000.

To get started building your own application, open your command-line shell (i.e. Terminal on a Mac or PowerShell on Windows) and navigate to the directory of your choice. Then create a new web application using the dotnet CLI and go to that directory:

dotnet new webapp -o your-application
cd your-application

Creating a new application in Visual Studio will work as well. You will use the command line here for the simplicity of the example.

Once the application has been created, you can start up the web application using the following command in.

dotnet run

The output should look similar to the following.

Building...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5146
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/Shared/your-application

The application will be accessible in your browser at http://localhost:{PortNumber} where the port number will be displayed in your shell window, i.e http://localhost:5146.

Press Control+C in the terminal window to stop the application from running.

Authentication

It’s always smart to leverage existing libraries as they are likely to be more secure and better handle edge cases. You’re going to add an OpenIdConnect library to the application. In your terminal window, make sure you’re in the root directory for your application and run the command below to add the library.

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

This application will contain two pages that require authorization. The Account page will display the user’s balance and the MakeChange page will make change for the user. To protect these pages you can use the Authorize filter attribute in the page model.

Create Account.cshtml and Account.cshtml.cs in the Pages directory. To protect the “Account” page you can use the Authorize filter attribute in the page model.

Copy the following code into Account.cshtml.cs:

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

namespace Pages;

[Authorize]
public class AccountsModel : PageModel
{
    private readonly ILogger<PrivacyModel> _logger;

    public AccountsModel(ILogger<PrivacyModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
      
    }
}

Copy the following code into Account.cshtml:

@page
@model AccountsModel
@{
    ViewData["Title"] = "Account";
}

<div style="flex: 1;">
<!-- Application page -->
    <div class="column-container">
        <div class="app-container">
            <h3>Your balance</h3>
            <div class="balance">$100.00</div>
        </div>
    </div>
</div>

Create MakeChange.cshtml and MakeChange.cshtml.cs in the Pages directory. You will use the same Authorize filter attribute to secure the page.

Copy the following code into MakeChange.cshtml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Pages
{
    [Authorize]
    public class MakeChangeModel : PageModel
    {
        public string Message { get; private set; } = "";
        public bool Error { get; private set; } = false;
        public string Amount { get; set; } = "0.00";

        public void OnGet()
        {
        }

        public void OnPost(string amount)
        {
            MakeChange(amount);
        }

        public void MakeChange(string amount)
        {
            try
            {
                Amount = amount;

                decimal remainingamount = Convert.ToDecimal(amount);

                Message = "We can make change for";

                var coins = new[] { // ordered
                    new { name = "quarters", nominal   = 0.25m },
                    new { name = "dimes", nominal      = 0.10m },
                    new { name = "nickels", nominal    = 0.05m },
                    new { name = "pennies", nominal   = 0.01m }
                };

                foreach (var coin in coins)
                {
                    int count = (int)(remainingamount / coin.nominal);
                    remainingamount -= count * coin.nominal;

                    Message += $" {count} {coin.name}";
                }

                Message += "!";

            }
            catch (Exception ex)
            {
                Message = @$"There was a problem converting the amount submitted. {ex.Message}";
            }
        }
    }
}

Copy the following code into MakeChange.cshtml:

@page
@model Pages.MakeChangeModel
@{
}

<div style="flex: 1;">
    <div class="column-container">
        <div class="app-container change-container">
            <h3>We Make Change</h3>
            <div class="change-message">
                @Model.Message
            </div>
            <form method="post">
                <div class="h-row">
                    <div class="change-label">Amount in USD: $</div>
                    <input class="change-input" name="amount" value="@Model.Amount"  />
                    <input class="change-submit" type="submit" value="Make Change" />
                </div>
            </form>
        </div>
    </div>
</div>

You will need a way to allow the user to logout of the application as well. You’ll build the logout functionality in the Logout page. This will:

  • Remove the session cookie.
  • Redirect to the FusionAuth OAuth logout endpoint.

FusionAuth will then destroy its session and redirect the user back to the app’s configured logout URL.

Add the following file to the Pages directory and call it Logout.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Pages
{
    public class LogoutModel : PageModel
    {
        private readonly ILogger<LogoutModel> _logger;
        private readonly IConfiguration _configuration;

        public LogoutModel(ILogger<LogoutModel> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        public IActionResult OnGet()
        {
            SignOut("cookie", "oidc");
            var host = _configuration["FusionAuthSettings:Authority"];
            var cookieName = _configuration["FusionAuthSettings:CookieName"];

            var clientId = _configuration["FusionAuthSettings:ClientId"];
            var url = host + "/oauth2/logout?client_id=" + clientId;
            Response.Cookies.Delete(cookieName);
            return Redirect(url);
        }
    }
}

OnGet is the important method. Here you sign out using a method of the authentication library, delete the JWT cookie, and send the user to the FusionAuth OAuth logout endpoint.

Now add Logout.cshtml to the Pages directory. No content is necessary. Just declare the page and model as shown below.

@page
@model LogoutModel
@{
}

Application Configuration

You will need to do a little plumbing to ensure things like having the correct settings and configuring the cookies properly to work over HTTP for the sample application.

App Settings

Replace the text in the appsettings.json file with the text below. The important part here is that we are adding the FusionAuthSettings section so that the code above will run with the correct configuration settings. Authority is just the location of the user identity server, in this case, FusionAuth. Urls ensures the application runs on the specified port, 5000.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Urls": "http://localhost:5000",
  "FusionAuthSettings": {
    "Authority": "http://localhost:9011",
    "CookieName": "mycookie",
    "ClientId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e",
    "ClientSecret": "super-secret-secret-that-should-be-regenerated-for-production"
  }
}

Program Settings

Replace the text in the Program.cs file. Since you are using unsecured cookies, you need to allow this in the configuration. Safari and Chrome require different settings and that is reflected in the code below.

var builder = WebApplication.CreateBuilder(args);
var startup = new Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);

var app = builder.Build();

// Add this before any other middleware that might write cookies
app.UseCookiePolicy();

app.UseCookiePolicy(new CookiePolicyOptions
{
    Secure = CookieSecurePolicy.Always,
    //safari does not work if this is not set here and chrome works as well
    MinimumSameSitePolicy = SameSiteMode.Unspecified
});

startup.Configure(app, builder.Environment);
app.Run();

Startup Settings

Add the Startup.cs file in the root directory of the application. This sets the correct port and manages unsecured cookies for the browsers. The CheckSameSite and DisallowsSameSiteNone functions help manage the cookie settings for the different browsers.

using System;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Logging;


public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
        services.AddRazorPages();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "cookie";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("cookie", options =>
            {
                options.Cookie.Name = Configuration["FusionAuthSettings:CookieName"];
                options.Cookie.SameSite = SameSiteMode.None;

            })
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = Configuration["FusionAuthSettings:Authority"];
                options.ClientId = Configuration["FusionAuthSettings:ClientId"];
                options.ClientSecret = Configuration["FusionAuthSettings:ClientSecret"];
                options.ResponseType = "code";
                options.RequireHttpsMetadata = false;
                options.Scope.Add("email");
            });

        services.Configure<CookiePolicyOptions>(options =>
        {
            options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
            options.OnAppendCookie = cookieContext =>
                CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
            options.OnDeleteCookie = cookieContext =>
                CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
        IdentityModelEventSource.ShowPII = true;
    }

    private void CheckSameSite(HttpContext httpContext, CookieOptions options)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        if (DisallowsSameSiteNone(userAgent))
        {
            options.SameSite = SameSiteMode.Unspecified;
            options.Secure = false;
        }
    }

    //  Read comments in https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
    public bool DisallowsSameSiteNone(string userAgent)
    {
        // Check if a null or empty string has been passed in, since this
        // will cause further interrogation of the useragent to fail.
        if (String.IsNullOrWhiteSpace(userAgent))
            return false;

        // Cover all iOS based browsers here. This includes:
        // - Safari on iOS 12 for iPhone, iPod Touch, iPad
        // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
        // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
        // All of which are broken by SameSite=None, because they use the iOS networking
        // stack.
        if (userAgent.Contains("CPU iPhone OS 12") ||
            userAgent.Contains("iPad; CPU OS 12"))
        {
            return true;
        }

        // Cover Mac OS X based browsers that use the Mac OS networking stack. 
        // This includes:
        // - Safari on Mac OS X.
        // This does not include:
        // - Chrome on Mac OS X
        // Because they do not use the Mac OS networking stack.
        if (userAgent.Contains("Macintosh; Intel Mac OS X") &&
            userAgent.Contains("Version/") && userAgent.Contains("Safari"))
        {
            return true;
        }

        // Cover Chrome 50-69, because some versions are broken by SameSite=None, 
        // and none in this range require it.
        // Note: this covers some pre-Chromium Edge versions, 
        // but pre-Chromium Edge does not require SameSite=None.
        if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
        {
            return true;
        }

        return false;
    }
}

There are a few settings applied to simplify the sample application. In a production application, do not allow cookies over http or keep the ClientSecret in a .json file for production applications. Get the ClientSecret from an environmental variable that has been set on a production machine or some other secure way.

This application will display different menu items for authenticated users and non authenticated users. To add this type of navigation update /Pages/Shared/_Layout.cshtml with the following:

<html>
<head>
  <meta charset="utf-8" />
  <title>FusionAuth OpenID .Net Web Example</title>
  <link rel="stylesheet" href="~/css/changebank.css">
</head>
<body>
    <div id="page-container">
        @if (User.Identity != null && User.Identity.IsAuthenticated == true)
        {
            <div id="page-header">
                <div id="logo-header">
                    <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
                    <div class="h-row">
                        <p class="header-email">@User.Claims.Where(x => x.Type == "email").First().Value</p>
                        <a class="button-lg" href="/Logout">Logout</a>
                    </div>
                </div>
                <div id="menu-bar" class="menu-bar">
                    <a class="menu-link" " href="/makechange">Make Change</a>
                    <a class="menu-link" href="/account">Account</a>
                </div>
            </div>
        }
        else
        {
            <div id="page-header">
                <div id="logo-header">
                    <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
                    <a class="button-lg" href="/Account">Login</a>
                </div>

                <div id="menu-bar" class="menu-bar">
                    <a class="menu-link">About</a>
                    <a class="menu-link">Services</a>
                    <a class="menu-link">Products</a>
                    <a class="menu-link" style="text-decoration-line: underline;">Home</a>
                </div>
            </div>
        }
        @RenderBody()
    </div>
</body>
</html>

Homepage

Update the default page.

Copy the following code into Pages/Index.cshtml.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Pages;

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
       
    }
}

Copy the following code into Index.cshtml and notice the logic to show a different view to users that have not logged in.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}


<div style="flex: 1;">
    <div class="column-container">
        <div class="content-container">
            <div style="margin-bottom: 100px;">
                <h1>Welcome to Changebank</h1>
                @if (User.Identity != null && User.Identity.IsAuthenticated == true)
                {
                    <p>To get started, please select from the menu.</p>
                }
                else
                {
                    <p>To get started, <a href="/Account">log in or create a new account</a>.</p>
                }
            </div>
        </div>
        <div style="flex: 0;">
            <img src="~/img/money.jpg" style="max-width: 800px;"/>
        </div>
    </div>
</div>

Styling

To give the pages the look and feel of an application, apply the following styling and images.

Add the changebank.css file in the wwwroot/css directory of the application.

h1 {
  color: #096324;
}

h3 {
  color: #096324;
  margin-top: 20px;
  margin-bottom: 40px;
}

a {
  color: #096324;
}

p {
  font-size: 18px;
}

.header-email {
  color: #096324;
  margin-right: 20px;
}

.fine-print {
  font-size: 16px;
}

body {
  font-family: sans-serif;
  padding: 0px;
  margin: 0px;
}

.h-row {
  display: flex;
  align-items: center;
}

#page-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

#page-header {
  flex: 0;
  display: flex;
  flex-direction: column;
}

#logo-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
}

.menu-bar {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  height: 35px;
  padding: 15px 50px 15px 30px;
  background-color: #096324;
  font-size: 20px;
}

.menu-link {
  font-weight: 600;
  color: #FFFFFF;
  margin-left: 40px;
}

.menu-link {
  font-weight: 600;
  color: #FFFFFF;
  margin-left: 40px;
}

.inactive {
  text-decoration-line: none;
}

.button-lg {
  width: 150px;
  height: 30px;
  background-color: #096324;
  color: #FFFFFF;
  font-size: 16px; 
  font-weight: 700; 
  border-radius: 10px; 
  text-align: center;
  padding-top: 10px;
  text-decoration-line: none;
}

.column-container {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}

.content-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 60px 20px 20px 40px;
}

.balance {
  font-size: 50px;
  font-weight: 800;
}

.change-label {
  font-size: 20px;
  margin-right: 5px;
}

.change-input {
  font-size: 20px;
  height: 40px;
  text-align: end;
  padding-right: 10px;
}

.change-submit {
  font-size: 15px;
  height: 40px;
  margin-left: 15px;
  border-radius: 5px;
}

.change-message {
  font-size: 20px;
  margin-bottom: 15px;
}

.error-message {
  font-size: 20px;
  color: #FF0000;
  margin-bottom: 15px;
}

.app-container {
  flex: 0;
  min-width: 440px;
  display: flex;
  flex-direction: column;
  margin-top: 40px;
  margin-left: 80px;
}

.change-container {
  flex: 1;
}

Copy the money image for the Index page. Run the following command in a terminal window. Make sure you are in the root directory for your application.

curl --create-dirs --output wwwroot/img/money.jpg https://raw.githubusercontent.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web/main/complete-app/app/assets/images/money.jpg

A Little Clean Up

Next we need to do a little clean up and make sure all the namespaces work together in the application.

Replace the text in the Pages/_ViewImports.cshtml file with the following code.

@namespace Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Replace the text in the Pages/Error.cshtml.cs file with the following code.

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Pages;

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
    }
}

Replace the text in the Pages/Privacy.cshtml.cs file with the following code.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Pages;

public class PrivacyModel : PageModel
{
    private readonly ILogger<PrivacyModel> _logger;

    public PrivacyModel(ILogger<PrivacyModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
    }
}

Run The Application

You can start up the .NET 7 application using this command in a terminal window from the root directory of your application:

dotnet run

You can now open up an incognito window and visit the app at http://localhost:5000. Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button.

Made it this far? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:

Security

Tenant and Application Management

Troubleshooting

If you run into an issue with cookies on Safari or other browsers, you might need to run the .NET application under SSL.

You can follow this guide to install development SSL certificates for your .NET environment.

Alternatively, you can run the project using Visual Studio, which will run the project using SSL.

If you do this, make sure to update the Authorized Redirect URL to reflect the https protocol. Also note that the project will probably run on a different port when using SSL, so you must update that as well. To do so, log into the administrative user interface, navigate to Applications, then click the Edit button on your application and navigate to the OAuth tab. You can have more than one URL.

If you change the name of the application from ‘your-application’ to some other name, you will need to make sure the correct namespace is used in all of your code.

There are multiple ways of deploying an application, but publishing ensures your deployment process is repeatable. You can use the RID catalog to build different versions of this application for different platforms.

If you have trouble running the application, please verify all the files are updated as instructed. The directory for the your-application tree should look like this:

└── your-application
    ├── appsettings.Development.json
    ├── appsettings.json
    ├── bin/
        ├── ...
    ├── obj/
        ├── ...
    ├── Pages/
    │   ├── _ViewImports.cshtml
    │   ├── _ViewStart.cshtml
    |   |── Account.cshtml
    |   |── Accuont.cshtml.cs
    │   ├── Error.cshtml
    │   ├── Error.cshtml.cs
    │   ├── Index.cshtml
    │   ├── Index.cshtml.cs
    │   ├── Logout.cshtml
    │   ├── Logout.cshtml.cs
    |   |── MakeChange.cshtml
    |   |── MakeChange.cshtml.cs
    │   ├── Privacy.cshtml
    │   ├── Privacy.cshtml.cs
    │   └── Shared/
    │       ├── _Layout.cshtml
    │       ├── _Layout.cshtml.css
    │       └── _ValidateScriptsPartial.cshtml
    ├── Program.cs
    ├── Properties/
        ├── ...
    ├── Startup.cs
    └── wwwroot/
        ├── ...

The full code is available here.