Published: January 07 2022

.NET 6.0 - User Registration and Login Tutorial with Example API

Tutorial built with .NET 6.0

Other versions available:

In this tutorial we'll go through an example .NET 6.0 API that supports user registration, login with JWT authentication and user CRUD operations.

EF Core Database Configuration

The API is configured with Entity Framework Core to use a local SQLite database in development and a SQL Server database in production, the database provider is assigned on startup in the .NET Program file, the database is then automatically generated/updated (if required) using Entity Framework Core migrations.

Code on GitHub

The tutorial project code is available on GitHub at https://github.com/cornflourblue/dotnet-6-registration-login-api.


.NET 6.0 Tutorial Contents


Tools required to run the .NET 6.0 Tutorial Example Locally

To develop and run .NET 6.0 applications locally, download and install the following:

  • .NET SDK - includes the .NET runtime and command line tools
  • Visual Studio Code - code editor that runs on Windows, Mac and Linux
  • C# extension for Visual Studio Code - adds support to VS Code for developing .NET applications


Install dotnet ef tools

The .NET Entity Framework Core tools (dotnet ef) are used to generate EF Core migrations, to install the EF Core tools globally run dotnet tool install -g dotnet-ef, or to update run dotnet tool update -g dotnet-ef.

For more info on EF Core tools see https://docs.microsoft.com/ef/core/cli/dotnet.

For more info on EF Core migrations see https://docs.microsoft.com/ef/core/managing-schemas/migrations.


Run the .NET 6.0 Tutorial API Locally

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/dotnet-6-registration-login-api
  2. Start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000. Follow the instructions below to test with Postman or hook up with one of the example single page applications available (Angular, Blazor, React or Vue).


Debugging in VS Code

You can start the application in debug mode in VS Code by opening the project root folder in VS Code and pressing F5 or by selecting Debug -> Start Debugging from the top menu. Running in debug mode allows you to attach breakpoints to pause execution and step through the application code. For more info on debugging .NET in VS Code see VS Code + .NET - Debug a .NET Web App in Visual Studio Code.

Before running in production

Before running in production also make sure that you update the Secret property in the appsettings.json file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).


Test the .NET 6.0 API with Postman

Postman is a great tool for testing APIs, you can download it at https://www.postman.com/downloads.

Below are instructions on how to use Postman to register a new user with the .NET 6.0 api, authenticate a user to get a JWT token, and then make authenticated requests with the JWT token to fetch all users and update a specific user.

How to register a new user with Postman

To register a new user with the api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to POST with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the register route of your local API - http://localhost:4000/users/register.
  4. Select the Body tab below the URL field, change the body type radio button to raw, and change the format dropdown selector to JSON.
  5. Enter a JSON object containing the required user properties in the Body textarea, e.g:
    {
        "firstName": "Jason",
        "lastName": "Watmore",
        "username": "jason",
        "password": "my-super-secret-password"
    }
  6. Click the Send button, you should receive a "200 OK" response with a "Registration successful" message.

Here's a screenshot of Postman after the request is sent and the new user has been registered:


How to authenticate a user with Postman

To authenticate a user with the api and get a JWT token follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to POST with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the authenticate route of your local API - http://localhost:4000/users/authenticate.
  4. Select the Body tab below the URL field, change the body type radio button to raw, and change the format dropdown selector to JSON.
  5. Enter a JSON object containing the username and password in the Body textarea:
    {
        "username": "jason",
        "password": "my-super-secret-password"
    }
  6. Click the Send button, you should receive a "200 OK" response with the user details including a JWT token in the response body, make a copy of the token value because we'll be using it in the next step to make an authenticated request.

Here's a screenshot of Postman after the request is sent and the user has been authenticated:


How to make an authenticated request to retrieve all users

To make an authenticated request using the JWT token from the previous step, follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to GET with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the users route of your local API - http://localhost:4000/users.
  4. Select the Authorization tab below the URL field, change the type to Bearer Token in the type dropdown selector, and paste the JWT token from the previous authenticate step into the Token field.
  5. Click the Send button, you should receive a "200 OK" response containing a JSON array with all the user records in the system.

Here's a screenshot of Postman after making an authenticated request to get all users:


How to update a user with Postman

To update a user with the api follow these steps:

  1. Open a new request tab by clicking the plus (+) button at the end of the tabs.
  2. Change the HTTP method to PUT with the dropdown selector on the left of the URL input field.
  3. In the URL field enter the address to the /users/{id} route with the id of the user you registered above, e.g - http://localhost:4000/users/1.
  4. Select the Authorization tab below the URL field, change the type to Bearer Token in the type dropdown selector, and paste the JWT token from the previous authenticate step into the Token field.
  5. Select the Body tab below the URL field, change the body type radio button to raw, and change the format dropdown selector to JSON.
  6. Enter a JSON object in the Body textarea containing the properties you want to update, for example to update the first and last names:
    {
        "firstName": "Foo",
        "lastName": "Bar"
    }
  7. Click the Send button, you should receive a "200 OK" response with the message "User updated successfully".

Here's a screenshot of Postman after the request is sent and the user has been updated:

 


Run an Angular client app with the .NET 6.0 Tutorial API

For full details about the example Angular application see the post Angular 10 - User Registration and Login Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the Angular 10 tutorial code from https://github.com/cornflourblue/angular-10-registration-login-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file.
  5. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the .NET 6.0 API that you already have running.


Run a Blazor WebAssembly (WASM) client app with the .NET 6.0 Tutorial API

For full details about the example Blazor application see the post Blazor WebAssembly - User Registration and Login Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/blazor-webassembly-registration-login-example
  2. Change the "fakeBackend" setting to "false" in the /wwwroot/appsettings.json file.
  3. Start the Blazor app by running dotnet run from the command line in the project root folder (where the BlazorApp.csproj file is located)
  4. Open a new browser tab and navigate to the URL http://localhost:5000, the Blazor app should be hooked up with the .NET 6.0 API that you already have running.

NOTE: To enable hot reloading during development so the Blazor app automatically restarts when a file is changed, start the app with the command dotnet watch run.


Run a React client application with the .NET 6.0 Tutorial API

For full details about the example React application see the post React + Recoil - User Registration and Login Example & Tutorial. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the React tutorial code from https://github.com/cornflourblue/react-recoil-registration-login-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.jsx file.
  5. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the .NET 6.0 API that you already have running.


Running a Vue client application with the .NET 6.0 Tutorial API

For full details about the example Vue application see the post Vue + Vuex - User Registration and Login Tutorial & Example. But to get up and running quickly just follow the below steps.

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the Vue tutorial code from https://github.com/cornflourblue/vue-vuex-registration-login-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Remove or comment out the 2 lines below the comment // setup fake backend located in the /src/index.js file.
  5. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application and it should be hooked up with the .NET 6.0 API that you already have running.


.NET 6.0 Tutorial Project Structure

The .NET tutorial project is organised into the following folders:

Authorization
Contains the classes responsible for implementing custom JWT authentication and authorization in the API.

Controllers
Define the end points / routes for the API, controller action methods are the entry points into the API for client applications via HTTP requests.

Migrations
Database migration files based on the classes in the /Entities folder that are used to automatically create and update the database for the API. Migrations are generated with the Entity Framework Core Tools for the .NET CLI (dotnet ef), the migrations in this example were generated with the following commands for the different database providers.
SQLite EF Core Migrations (Windows/MacOS):
dotnet ef migrations add InitialCreate --context SqliteDataContext --output-dir Migrations/SqliteMigrations
SQL Server EF Core Migrations (Windows Command):
set ASPNETCORE_ENVIRONMENT=Production
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations

SQL Server EF Core Migrations (Windows PowerShell):
$env:ASPNETCORE_ENVIRONMENT="Production"
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations

SQL Server EF Core Migrations (MacOS):
ASPNETCORE_ENVIRONMENT=Production dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations.

Models
Represent request and response models for controller methods, request models define the parameters for incoming requests, and response models define the data that is returned.

Services
Contain business logic, validation and database access code.

Entities
Represent the application data that is stored in the database.
Entity Framework Core (EF Core) maps relational data from the database to instances of C# entity objects in the application for data management and CRUD operations.

Helpers
Anything that doesn't fit into the above folders.

Properties
Contains the launchSettings.json file that sets the environment (ASPNETCORE_ENVIRONMENT) to Development by default when running the API on your local machine.

Click any of the below links to jump down to a description of each file along with its code:

 

.NET Allow Anonymous Attribute

Path: /Authorization/AllowAnonymousAttribute.cs

The custom [AllowAnonymous] attribute is used to allow anonymous access to specified action methods of controllers that are decorated with the [Authorize] attribute. It's used in the users controller to allow anonymous access to the register and login action methods. The custom authorize attribute below skips authorization if the action method is decorated with [AllowAnonymous].

I created a custom allow anonymous (instead of using the built in one) for consistency and to avoid ambiguous reference errors between namespaces.

namespace WebApi.Authorization;

[AttributeUsage(AttributeTargets.Method)]
public class AllowAnonymousAttribute : Attribute
{ }
 

.NET Custom Authorize Attribute

Path: /Authorization/AuthorizeAttribute.cs

The custom [Authorize] attribute is used to restrict access to controllers or specified action methods. Only authorized requests are allowed to access action methods that are decorated with the [Authorize] attribute.

When a controller is decorated with the [Authorize] attribute all action methods are restricted to authorized requests, except for methods decorated with the custom [AllowAnonymous] attribute above.

Authorization is performed by the OnAuthorization method which checks if there is an authenticated user attached to the current request (context.HttpContext.Items["User"]). An authenticated user is attached by the custom jwt middleware if the request contains a valid JWT access token.

On successful authorization no action is taken and the request is passed through to the controller action method, if authorization fails a 401 Unauthorized response is returned.

namespace WebApi.Authorization;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.Entities;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // skip authorization if action is decorated with [AllowAnonymous] attribute
        var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
        if (allowAnonymous)
            return;

        // authorization
        var user = (User)context.HttpContext.Items["User"];
        if (user == null)
            context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
    }
}
 

.NET Custom JWT Middleware

Path: /Authorization/JwtMiddleware.cs

The custom JWT middleware extracts the JWT token from the request Authorization header (if there is one) and validates it with the jwtUtils.ValidateToken() method. If validation is successful the user id from the token is returned, and the authenticated user object is attached to the HttpContext.Items collection to make it accessible within the scope of the current request.

If token validation fails or there is no token, the request is anonymous and only allowed to access public routes because there isn't an authenticated user object attached to the HTTP context. The authorization logic that checks the user object is attached is located in the custom authorize attribute. If an anonymous request is sent to a secure route a 401 Unauthorized response is returned by the authorize attribute.

namespace WebApi.Authorization;

using WebApi.Services;

public class JwtMiddleware
{
    private readonly RequestDelegate _next;

    public JwtMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        var userId = jwtUtils.ValidateToken(token);
        if (userId != null)
        {
            // attach user to context on successful jwt validation
            context.Items["User"] = userService.GetById(userId.Value);
        }

        await _next(context);
    }
}
 

.NET JWT Utils

Path: /Authorization/JwtUtils.cs

The JWT utils class contains methods for generating and validating JWT tokens.

The GenerateToken() method generates a JWT token with the id of the specified user as the "id" claim, meaning the token payload will contain the property "id": <userId> (e.g. "id": 1).

The ValidateToken() method attempts to validate the provided JWT token and return the user id ("id") from the token claims. If validation fails null is returned.

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using WebApi.Entities;
using WebApi.Helpers;

public interface IJwtUtils
{
    public string GenerateToken(User user);
    public int? ValidateToken(string token);
}

public class JwtUtils : IJwtUtils
{
    private readonly AppSettings _appSettings;

    public JwtUtils(IOptions<AppSettings> appSettings)
    {
        _appSettings = appSettings.Value;
    }

    public string GenerateToken(User user)
    {
        // generate token that is valid for 7 days
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddDays(7),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    public int? ValidateToken(string token)
    {
        if (token == null) 
            return null;

        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);

            var jwtToken = (JwtSecurityToken)validatedToken;
            var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

            // return user id from JWT token if validation successful
            return userId;
        }
        catch
        {
            // return null if validation fails
            return null;
        }
    }
}
 

.NET Users Controller

Path: /Controllers/UsersController.cs

The .NET users controller defines and handles all routes / endpoints for the api that relate to users, this includes authentication, registration and standard CRUD operations. Within each route the controller calls the user service to perform the action required which keeps the controller 'lean' and completely separated from the database / persistence code.

Controller action methods are secure by default with the custom [Authorize] attribute on the class, the Authenticate and Register methods allow public access by overriding the [Authorize] attribute on the controller with an [AllowAnonymous] attribute on each action method. I chose this approach for security to prevent a route accidentally being made public, any new action method will be secure by default unless explicitly made public.

namespace WebApi.Controllers;

using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using WebApi.Authorization;
using WebApi.Helpers;
using WebApi.Models.Users;
using WebApi.Services;

[Authorize]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private IUserService _userService;
    private IMapper _mapper;
    private readonly AppSettings _appSettings;

    public UsersController(
        IUserService userService,
        IMapper mapper,
        IOptions<AppSettings> appSettings)
    {
        _userService = userService;
        _mapper = mapper;
        _appSettings = appSettings.Value;
    }

    [AllowAnonymous]
    [HttpPost("authenticate")]
    public IActionResult Authenticate(AuthenticateRequest model)
    {
        var response = _userService.Authenticate(model);
        return Ok(response);
    }

    [AllowAnonymous]
    [HttpPost("register")]
    public IActionResult Register(RegisterRequest model)
    {
        _userService.Register(model);
        return Ok(new { message = "Registration successful" });
    }

    [HttpGet]
    public IActionResult GetAll()
    {
        var users = _userService.GetAll();
        return Ok(users);
    }

    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        var user = _userService.GetById(id);
        return Ok(user);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, UpdateRequest model)
    {
        _userService.Update(id, model);
        return Ok(new { message = "User updated successfully" });
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        _userService.Delete(id);
        return Ok(new { message = "User deleted successfully" });
    }
}
 

.NET User Entity

Path: /Entities/User.cs

The user entity class represents the data stored in the database for users.

Entity classes are also used to pass data between different parts of the application (e.g. between services and controllers) and can be used to return http response data from controller action methods.

The [JsonIgnore] attribute prevents the PasswordHash property from being serialized and returned in api responses.

namespace WebApi.Entities;

using System.Text.Json.Serialization;

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }

    [JsonIgnore]
    public string PasswordHash { get; set; }
}
 

.NET Custom App Exception

Path: /Helpers/AppException.cs

The app exception is a custom exception class used to differentiate between handled and unhandled exceptions in the .NET API. Handled exceptions are generated by application code and used to return friendly error messages, for example business logic or validation exceptions caused by invalid request parameters, whereas unhandled exceptions are generated by the .NET framework or caused by bugs in application code.

Exceptions are handled in the example by the global error handler middleware, and some app exceptions are thrown by the user service when validation fails.

namespace WebApi.Helpers;

using System.Globalization;

// custom exception class for throwing application specific exceptions (e.g. for validation) 
// that can be caught and handled within the application
public class AppException : Exception
{
    public AppException() : base() {}

    public AppException(string message) : base(message) { }

    public AppException(string message, params object[] args) 
        : base(String.Format(CultureInfo.CurrentCulture, message, args))
    {
    }
}
 

.NET App Settings

Path: /Helpers/AppSettings.cs

The app settings class contains properties defined in the appsettings.json file and is used for accessing application settings via objects that are injected into classes using the .NET 6.0 dependency injection (DI) system. For example the users controller accesses app settings via an IOptions<AppSettings> appSettings object that is injected into its constructor.

namespace WebApi.Helpers;

public class AppSettings
{
    public string Secret { get; set; }
}
 

.NET AutoMapper Profile

Path: /Helpers/AutoMapperProfile.cs

The automapper profile contains the mapping configuration used by the application, AutoMapper is a package available on Nuget that enables automatic mapping from one type of class to another. In this example we're using it to map between User entities and a few different model types - AuthenticateResponse, RegisterRequest and UpdateRequest.

All automapper profiles in the project are run on startup with a call to services.AddAutoMapper(typeof(Program)) in the .NET Program file. The type parameter typeof(Program) tells automapper to look for all automapper profiles in the current project assembly, any type in the project could be used as it is simply a marker type for the assembly.

namespace WebApi.Helpers;

using AutoMapper;
using WebApi.Entities;
using WebApi.Models.Users;

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        // User -> AuthenticateResponse
        CreateMap<User, AuthenticateResponse>();

        // RegisterRequest -> User
        CreateMap<RegisterRequest, User>();

        // UpdateRequest -> User
        CreateMap<UpdateRequest, User>()
            .ForAllMembers(x => x.Condition(
                (src, dest, prop) =>
                {
                    // ignore null & empty string properties
                    if (prop == null) return false;
                    if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;

                    return true;
                }
            ));
    }
}
 

.NET Data Context

Path: /Helpers/DataContext.cs

The data context is used for accessing application data through Entity Framework Core and is configured to connect to a SQL Server database. It derives from the EF Core DbContext class and has a public Users property for accessing and managing user data. The data context is used by the user service for handling all low level data operations.

In development environments the api is configured to use the SQLite data context which inherits from DataContext and overrides the db provider to connect to a local SQLite database instead of SQL Server.

namespace WebApi.Helpers;

using Microsoft.EntityFrameworkCore;
using WebApi.Entities;

public class DataContext : DbContext
{
    protected readonly IConfiguration Configuration;

    public DataContext(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // connect to sql server database
        options.UseSqlServer(Configuration.GetConnectionString("WebApiDatabase"));
    }

    public DbSet<User> Users { get; set; }
}
 

.NET Global Error Handler Middleware

Path: /Helpers/ErrorHandlerMiddleware.cs

The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the .NET 6 API. It's configured as middleware in the Program.cs file.

Errors of type AppException are treated as custom (app specific) errors that return a 400 Bad Request response, the .NET built-in KeyNotFoundException class is used to return 404 Not Found responses, all other exceptions are unhandled and return a 500 Internal Server Error response.

See the user service for examples of custom errors and not found errors thrown by the api.

namespace WebApi.Helpers;

using System.Net;
using System.Text.Json;

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception error)
        {
            var response = context.Response;
            response.ContentType = "application/json";

            switch(error)
            {
                case AppException e:
                    // custom application error
                    response.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;
                case KeyNotFoundException e:
                    // not found error
                    response.StatusCode = (int)HttpStatusCode.NotFound;
                    break;
                default:
                    // unhandled error
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }

            var result = JsonSerializer.Serialize(new { message = error?.Message });
            await response.WriteAsync(result);
        }
    }
}
 

.NET SQLite Data Context

Path: /Helpers/SqliteDataContext.cs

The SQLite data context is used by the API in development environments, it inherits from the main data context and overrides the provider to use SQLite instead of SQL Server.

namespace WebApi.Helpers;

using Microsoft.EntityFrameworkCore;

public class SqliteDataContext : DataContext
{
    public SqliteDataContext(IConfiguration configuration) : base(configuration) { }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // connect to sqlite database
        options.UseSqlite(Configuration.GetConnectionString("WebApiDatabase"));
    }
}
 

EF Core SQLite Migrations

Path: /Migrations/SqliteMigrations

Entity Framework Core migrations for the SQLite database provider used in development environments.

The migrations in this example were generated with the command dotnet ef migrations add InitialCreate --context SqliteDataContext --output-dir Migrations/SqliteMigrations.

The migrations are generated for SQLite because the SqliteDataContext class is configured to connect to a SQLite database. The SqliteDataContext inherits from the main DataContext class and overrides the provider to use SQLite instead of SQL Server. This enables the project to support multiple different database providers for different environments.

To install EF Core tools for the .NET CLI globally run dotnet tool install -g dotnet-ef, or to update run dotnet tool update -g dotnet-ef.

 

EF Core SQL Server Migrations

Path: /Migrations/SqlServerMigrations

Entity Framework Core migrations for the SQL Server database provider used in production environments.

The migrations in this example were generated with the below command.

Windows Command:
set ASPNETCORE_ENVIRONMENT=Production
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations

Windows PowerShell:
$env:ASPNETCORE_ENVIRONMENT="Production"
dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations

MacOS:
ASPNETCORE_ENVIRONMENT=Production dotnet ef migrations add InitialCreate --context DataContext --output-dir Migrations/SqlServerMigrations

The environment variable ASPNETCORE_ENVIRONMENT must be set to Production for the SQL Server DataContext class to be configured with the .NET dependency injection system, see the data context configuration code in the .NET Program file.

To install EF Core tools for the .NET CLI globally run dotnet tool install -g dotnet-ef, or to update run dotnet tool update -g dotnet-ef.

 

.NET Authenticate Request Model

Path: /Models/Users/AuthenticateRequest.cs

The authenticate request model defines the parameters for incoming POST requests to the /users/authenticate route, it is attached to the route by setting it as the parameter to the Authenticate action method of the users controller. When an HTTP POST request is received by the route, the data from the body is bound to an instance of the AuthenticateRequest class, validated and passed to the method.

.NET Data Annotations are used to automatically handle model validation, the [Required] attribute sets both the username and password as required fields so if either are missing a validation error message is returned from the API.

namespace WebApi.Models.Users;

using System.ComponentModel.DataAnnotations;

public class AuthenticateRequest
{
    [Required]
    public string Username { get; set; }

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

.NET Authenticate Response Model

Path: /Models/Users/AuthenticateResponse.cs

The authenticate response model defines the data returned by the Authenticate method of the users controller. It includes basic user details and a JWT token.

namespace WebApi.Models.Users;

public class AuthenticateResponse
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public string Token { get; set; }
}
 

Register Request Model

Path: /Models/Users/RegisterRequest.cs

The register request model defines the parameters for incoming POST requests to the /users/register route, it is attached to the route by setting it as the parameter to the Register action method of the users controller. When an HTTP POST request is received by the route, the data from the body is bound to an instance of the RegisterRequest class, validated and passed to the method.

.NET Data Annotations are used to automatically handle model validation, the [Required] attribute sets all fields as required so if any are missing a validation error message is returned from the API.

namespace WebApi.Models.Users;

using System.ComponentModel.DataAnnotations;

public class RegisterRequest
{
    [Required]
    public string FirstName { get; set; }

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

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

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

Update Request Model

Path: /Models/Users/UpdateRequest.cs

The update request model defines the parameters for incoming PUT requests to the /users/{id} route, it is attached to the route by setting it as the parameter to the Update action method of the users controller. When an HTTP PUT request is received by the route, the data from the body is bound to an instance of the UpdateRequest class, validated and passed to the method.

None of the properties have the [Required] attribute so they are all optional. Omitted fields are ignored when a user is updated, configuration for this is in the UpdateRequest -> User mapping of the automapper profile.

namespace WebApi.Models.Users;

public class UpdateRequest
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
 

.NET launchSettings.json

Path: /Properties/launchSettings.json

Configuration file that contains settings and launch profiles used when launching the application on your local machine. The first profile in the file (Development) is used by default when starting the application with dotnet run, the file can contain multiple profiles and a different launch profile can be specified with the command line argument --launch-profile "PROFILE_NAME".

The launchSettings.json file doesn't get deployed, it is only used on your local dev machine. For more info see https://docs.microsoft.com/aspnet/core/fundamentals/environments?view=aspnetcore-6.0#lsj.

{
    "profiles": {
        "Development": {
            "commandName": "Project",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}
 

.NET User Service

Path: /Services/UserService.cs

The user service is responsible for all database interaction and core business logic related to user login, registration and CRUD operations.

The top of the file contains an interface that defines the user service, just below that is the concrete user service class that implements the interface. BCrypt is used to hash and verify passwords, for more info see .NET 6.0 - Hash and Verify Passwords with BCrypt.

namespace WebApi.Services;

using AutoMapper;
using BCrypt.Net;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Models.Users;

public interface IUserService
{
    AuthenticateResponse Authenticate(AuthenticateRequest model);
    IEnumerable<User> GetAll();
    User GetById(int id);
    void Register(RegisterRequest model);
    void Update(int id, UpdateRequest model);
    void Delete(int id);
}

public class UserService : IUserService
{
    private DataContext _context;
    private IJwtUtils _jwtUtils;
    private readonly IMapper _mapper;

    public UserService(
        DataContext context,
        IJwtUtils jwtUtils,
        IMapper mapper)
    {
        _context = context;
        _jwtUtils = jwtUtils;
        _mapper = mapper;
    }

    public AuthenticateResponse Authenticate(AuthenticateRequest model)
    {
        var user = _context.Users.SingleOrDefault(x => x.Username == model.Username);

        // validate
        if (user == null || !BCrypt.Verify(model.Password, user.PasswordHash))
            throw new AppException("Username or password is incorrect");

        // authentication successful
        var response = _mapper.Map<AuthenticateResponse>(user);
        response.Token = _jwtUtils.GenerateToken(user);
        return response;
    }

    public IEnumerable<User> GetAll()
    {
        return _context.Users;
    }

    public User GetById(int id)
    {
        return getUser(id);
    }

    public void Register(RegisterRequest model)
    {
        // validate
        if (_context.Users.Any(x => x.Username == model.Username))
            throw new AppException("Username '" + model.Username + "' is already taken");

        // map model to new user object
        var user = _mapper.Map<User>(model);

        // hash password
        user.PasswordHash = BCrypt.HashPassword(model.Password);

        // save user
        _context.Users.Add(user);
        _context.SaveChanges();
    }

    public void Update(int id, UpdateRequest model)
    {
        var user = getUser(id);

        // validate
        if (model.Username != user.Username && _context.Users.Any(x => x.Username == model.Username))
            throw new AppException("Username '" + model.Username + "' is already taken");

        // hash password if it was entered
        if (!string.IsNullOrEmpty(model.Password))
            user.PasswordHash = BCrypt.HashPassword(model.Password);

        // copy model to user and save
        _mapper.Map(model, user);
        _context.Users.Update(user);
        _context.SaveChanges();
    }

    public void Delete(int id)
    {
        var user = getUser(id);
        _context.Users.Remove(user);
        _context.SaveChanges();
    }

    // helper methods

    private User getUser(int id)
    {
        var user = _context.Users.Find(id);
        if (user == null) throw new KeyNotFoundException("User not found");
        return user;
    }
}
 

.NET 6 App Settings (Development)

Path: /appsettings.Development.json

Configuration file with application settings that are specific to the development environment, it includes the WebApiDatabase connection string for the local SQLite development database which overrides the connection string in the base appsettings.json file.

{
    "ConnectionStrings": {
        "WebApiDatabase": "Data Source=LocalDatabase.db"
    }
}
 

.NET 6 App Settings

Path: /appsettings.json

Base configuration file containing default application settings for all environments (unless overridden in environment specific config). Includes placeholder for your production SQL Server connection string.

IMPORTANT: The "Secret" property is used by the API to sign and verify JWT tokens for authentication, update it with your own random string to ensure nobody else can generate a JWT to gain unauthorized access to your application. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from https://www.guidgenerator.com/).

{
    "AppSettings": {
        "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
    },
    "ConnectionStrings": {
        "WebApiDatabase": "ENTER PRODUCTION SQL SERVER CONNECTION STRING HERE"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}
 

OmniSharp Config

Path: /omnisharp.json

This file contains configuration options for the C# extension in VS Code. The useBundledOnly option tells the C# extension to use the bundled version of MSBuild instead of the global version to prevent errors if you have an older version of MSBuild installed globally (e.g. as part of Visual Studio).

{
    "msbuild": {
        "useBundledOnly": true
    }
}
 

.NET 6 Program

Path: /Program.cs

The .NET 6 Program file contains top-level statements which are converted by the new C# 10 compiler into a Main() method and Program class for the .NET program. The Main() method is the entry point for a .NET application, when an app is started it searches for the Main() method to begin execution. The top-level statements can be located anywhere in the project but are typically placed in the Program.cs file, only one file can contain top-level statements within a .NET application.

The WebApplication class handles app startup, lifetime management, web server configuration and more. A WebApplicationBuilder is first created by calling the static method WebApplication.CreateBuilder(args), the builder is used to configure services for dependency injection (DI), a WebApplication instance is created by calling builder.Build(), the app instance is used to configure the HTTP request pipeline (middleware), then the app is started by calling app.Run().

I wrapped the add services... and configure HTTP... sections in curly brackets {} to group them together visually, the brackets are completely optional.

Internally the WebApplicationBuilder class calls the ConfigureWebHostDefaults() extension method which configures hosting for the web app including setting Kestrel as the web server, adding host filtering middleware and enabling IIS integration. For more info on the default builder settings see https://docs.microsoft.com/aspnet/core/fundamentals/host/generic-host#default-builder-settings.

using Microsoft.EntityFrameworkCore;
using WebApi.Authorization;
using WebApi.Helpers;
using WebApi.Services;

var builder = WebApplication.CreateBuilder(args);

// add services to DI container
{
    var services = builder.Services;
    var env = builder.Environment;
 
    // use sql server db in production and sqlite db in development
    if (env.IsProduction())
        services.AddDbContext<DataContext>();
    else
        services.AddDbContext<DataContext, SqliteDataContext>();
 
    services.AddCors();
    services.AddControllers();

    // configure automapper with all automapper profiles from this assembly
    services.AddAutoMapper(typeof(Program));

    // configure strongly typed settings object
    services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

    // configure DI for application services
    services.AddScoped<IJwtUtils, JwtUtils>();
    services.AddScoped<IUserService, UserService>();
}

var app = builder.Build();

// migrate any database changes on startup (includes initial db creation)
using (var scope = app.Services.CreateScope())
{
    var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();    
    dataContext.Database.Migrate();
}

// configure HTTP request pipeline
{
    // global cors policy
    app.UseCors(x => x
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader());

    // global error handler
    app.UseMiddleware<ErrorHandlerMiddleware>();

    // custom jwt auth middleware
    app.UseMiddleware<JwtMiddleware>();

    app.MapControllers();
}

app.Run("http://localhost:4000");
 

.NET 6 Web Api csproj

Path: /WebApi.csproj

The csproj (C# project) is an MSBuild based file that contains target framework and NuGet package dependency information for the application. The ImplicitUsings feature is enabled which tells the compiler to auto generate a set of global using directives based on the project type, removing the need to include a lot of common using statements. The global using statements are auto generated when you build the project and can be found in the file /obj/Debug/net6.0/WebApi.GlobalUsings.g.cs.

For more info on the C# project file see .NET + MSBuild - C# Project File (.csproj) in a Nutshell.

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="AutoMapper" Version="11.0.0" />
        <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
        <PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
    </ItemGroup>
</Project>

 


Need Some .NET Help?

Search fiverr for freelance .NET developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by