Published: July 29 2021

.NET 5.0 - Role Based Authorization Tutorial with Example API

Tutorial built with .NET 5.0

Other versions available:

In this tutorial we'll go through a simple example of how to implement role based authorization / access control in a .NET 5.0 API with C#. The example builds on another tutorial I posted recently which focuses on JWT authentication in .NET 5.0, this version has been extended to include role based authorization / access control on top of the JWT authentication.

API endpoints

The example API has just three endpoints/routes to demonstrate authentication and role based authorization:

  • /users/authenticate - public route that accepts HTTP POST requests with username and password in the body. If the username and password are correct then a JWT authentication token is returned.
  • /users - secure route restricted to "Admin" users only, it accepts HTTP GET requests and returns a list of all users if the HTTP Authorization header contains a valid JWT token and the user is in the "Admin" role. If there is no auth token, the token is invalid or the user is not in the "Admin" role a 401 Unauthorized response is returned.
  • /users/{id} - secure route restricted to authenticated users in any role, it accepts HTTP GET requests and returns the user record for the specified id parameter if authorization is successful. Note that "Admin" users can access all user records, while other roles (e.g. "User") can only access their own user record.

EF Core InMemory database for testing

To keep the api code as simple as possible, it is configured to use the EF Core InMemory database provider which allows Entity Framework Core to create and connect to an in-memory database rather than you having to install a real db server. This can be easily switched out to a real db provider when you're ready to work with a database such as SQL Server, Oracle, MySql etc. For an example api that uses SQLite in development and SQL Server in production see .NET 5.0 - Simple API for Authentication, Registration and User Management.

Code on GitHub

The tutorial project is available on GitHub at https://github.com/cornflourblue/dotnet-5-role-based-authorization-api.

Tutorial Contents


Tools required to run the .NET 5.0 Role Based Authorization Example Locally

To develop and run .NET 5.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


Run the .NET Role Based Authorization API Locally

  1. Download or clone the tutorial project code from https://github.com/cornflourblue/dotnet-5-role-based-authorization-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, React or Vue).

NOTE: To find out how to easily launch the API in debug mode with VS Code see VS Code + .NET - Debug a .NET Web App in Visual Studio Code and skip to the step: Generate tasks.json and launch.json.


Test the .NET 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 authenticate the "admin" test user to get a JWT token from the api, and then make an authenticated request with the token to retrieve a list of users from the api.

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 admin username and password in the Body textarea:
    {
        "username": "admin",
        "password": "admin"
    }
  6. Click the Send button, you should receive a "200 OK" response containing the user details and a JWT token in the response body.

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 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 (just the two test users in the example).

NOTE: This route is restricted to users in the Admin role, other users will receive a 401 Unauthorized response.

Screenshot of Postman after making an authenticated request to get all users:


Run an Angular client app with the .NET Role Based Auth API

For full details about the example Angular application see the post Angular 10 - Role Based Authorization Tutorial with 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 Angular 10 tutorial code from https://github.com/cornflourblue/angular-10-role-based-authorization-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 Angular example application and it should be hooked up with the .NET 5.0 Role Based Authorization API that you already have running.


Run a React client app with the .NET Role Based Auth API

For full details about the example React application see the post React - Role Based Authorization Tutorial with 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 React tutorial code from https://github.com/cornflourblue/react-role-based-authorization-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 React example application and it should be hooked up with the .NET 5.0 Role Based Authorization API that you already have running.


Run a Vue.js client app with the .NET Role Based Auth API

For full details about the example Vue.js application see the post Vue.js - Role Based Authorization Tutorial with 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.js tutorial code from https://github.com/cornflourblue/vue-role-based-authorization-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 Vue.js example application and it should be hooked up with the .NET 5.0 Role Based Authorization API that you already have running.
 

.NET 5.0 Role Based Access Control Project Structure

The tutorial project is organised into the following folders:

Authorization
Contains the classes responsible for implementing custom JWT authentication and role based authorization in the api.

Controllers
Define the endpoints / routes for the web api, controllers are the gateways into the web api for client applications via http requests.

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 to be used within the application for data management and CRUD operations.

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

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

 

.NET Custom 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 authenticate action method. 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 with the custom authorize attribute and to avoid ambiguous reference errors between namespaces.

using System;

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

.NET Custom Authorize Attribute

Path: /Authorization/AuthorizeAttribute.cs

The custom [Authorize] attribute is added to controller action methods that require the user to be authenticated and optionally have a specified role. If a role is specified (e.g. [Authorize(Role.Admin)]) then the route is restricted to users in that role, otherwise the route is restricted to all authenticated users regardless of role.

When a controller is decorated with the [Authorize] attribute all action methods in the controller 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"]) and that the user is in an authorized role (if specified).

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.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Collections.Generic;
using System.Linq;
using WebApi.Entities;

namespace WebApi.Authorization
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeAttribute : Attribute, IAuthorizationFilter
    {
        private readonly IList<Role> _roles;

        public AuthorizeAttribute(params Role[] roles)
        {
            _roles = roles ?? new Role[] { };
        }

        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 || (_roles.Any() && !_roles.Contains(user.Role)))
            {
                // not logged in or role not authorized
                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 only allowed to access public (anonymous) routes because there isn't an authenticated user object attached to the HTTP context. The authorization logic that checks if the user object is attached is located in the custom authorize attribute, and if authorization fails it returns a 401 Unauthorized response.

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System.Linq;
using System.Threading.Tasks;
using WebApi.Helpers;
using WebApi.Services;

namespace WebApi.Authorization
{
    public class JwtMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly AppSettings _appSettings;

        public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
        {
            _next = next;
            _appSettings = appSettings.Value;
        }

        public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
        {
            var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
            var userId = jwtUtils.ValidateJwtToken(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 GenerateJwtToken() method returns a JWT token that is valid for 7 days, it contains 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 token is created with the JwtSecurityTokenHandler class and digitally signed using the secret key stored in the app settings file.

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

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

namespace WebApi.Authorization
{
    public interface IJwtUtils
    {
        public string GenerateJwtToken(User user);
        public int? ValidateJwtToken(string token);
    }

    public class JwtUtils : IJwtUtils
    {
        private readonly AppSettings _appSettings;

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

        public string GenerateJwtToken(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? ValidateJwtToken(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 and standard CRUD operations. Within each route the controller calls the user service to perform the action required, this enables the controller to stay 'lean' and completely separated from the business logic and data access code.

The controller actions are secured with JWT using the [Authorize] attribute, with the exception of the Authenticate() method which allows public access by overriding the [Authorize] attribute on the controller with the [AllowAnonymous] attribute on the action method. I chose this approach so any new action methods added to the controller will be secure by default unless explicitly made public. Auth logic is located in the custom authorize attribute.

If roles are specified in the authorize attribute (e.g. [Authorize(Role.Admin)]) then the route is restricted to users in the specified role / roles.

The GetById(int id) action method contains some extra custom authorization logic which allows admin users to access any user record, but only allows normal users to access their own record.

using Microsoft.AspNetCore.Mvc;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Models.Users;
using WebApi.Services;

namespace WebApi.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class UsersController : ControllerBase
    {
        private IUserService _userService;

        public UsersController(IUserService userService)
        {
            _userService = userService;
        }

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

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

        [HttpGet("{id:int}")]
        public IActionResult GetById(int id)
        {
            // only admins can access other user records
            var currentUser = (User)HttpContext.Items["User"];
            if (id != currentUser.Id && currentUser.Role != Role.Admin)
                return Unauthorized(new { message = "Unauthorized" });

            var user =  _userService.GetById(id);
            return Ok(user);
        }
    }
}
 

.NET Role Enum

Path: /Entities/Role.cs

The role enum defines all the available roles in the example api. I created it to avoid passing roles around as strings, so instead of 'Admin' we can use Role.Admin.

namespace WebApi.Entities
{
    public enum Role
    {
        Admin,
        User
    }
}
 

.NET User Entity

Path: /Entities/User.cs

The user entity class represents the data for a user in the application. Entity classes represent the core data of a .NET app and are commonly used with an ORM such as Entity Framework to map to data stored in a relational database (e.g. SQL Server, MySQL, SQLite etc). Entities are also used to return HTTP response data from controller action methods and to pass data between different parts of the application (e.g. between services and controllers). If multiple types of entities or other custom data is required to be returned from a controller method then a custom model class should be created in the Models folder for the response.

The PasswordHash property is decorated with the [JsonIgnore] attribute to prevent it from being returned in API HTTP responses.

using System.Text.Json.Serialization;

namespace WebApi.Entities
{
    public class User
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Username { get; set; }
        public Role Role { get; set; }

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

.NET 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.

using System;
using System.Globalization;

namespace WebApi.Helpers
{
    // 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 Class

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 built in dependency injection (DI) system. For example the user service accesses app settings via an IOptions<AppSettings> appSettings object that is injected into the constructor.

Mapping of configuration sections to classes is done in the ConfigureServices() method of the Startup.cs file.

namespace WebApi.Helpers
{
    public class AppSettings
    {
        public string Secret { get; set; }
    }
}
 

.NET Data Context

Path: /Helpers/DataContext.cs

The data context class is used for accessing application data through Entity Framework. It derives from the Entity Framework 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 (CRUD) operations.

options.UseInMemoryDatabase() configures Entity Framework to create and connect to an in-memory database so the API can be tested without a real database, this can be easily updated to connect to a real db server such as SQL Server, Oracle, MySql etc. For an example api that uses SQLite in development and SQL Server in production see .NET 5.0 - Simple API for Authentication, Registration and User Management.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using WebApi.Entities;

namespace WebApi.Helpers
{
    public class DataContext : DbContext
    {
        public DbSet<User> Users { get; set; }

        private readonly IConfiguration Configuration;

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

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            // in memory database used for simplicity, change to a real db for production applications
            options.UseInMemoryDatabase("TestDb");
        }
    }
}
 

.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 api. It's configured as middleware in the Configure() method of the project startup class.

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.

using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;

namespace WebApi.Helpers
{
    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 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.

using System.ComponentModel.DataAnnotations;

namespace WebApi.Models.Users
{
    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 after successful authentication. It includes basic user details and a JWT token.

using WebApi.Entities;

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 Role Role { get; set; }
        public string Token { get; set; }

        public AuthenticateResponse(User user, string token)
        {
            Id = user.Id;
            FirstName = user.FirstName;
            LastName = user.LastName;
            Username = user.Username;
            Role = user.Role;
            Token = token;
        }
    }
}
 

.NET User Service

Path: /Services/UserService.cs

The user service contains the core logic for authentication, generating JWT tokens and fetching user data.

The top of the UserService.cs file contains the IUserService interface which defines the public methods for the user service, below the interface is the concrete UserService class that implements the interface.

The Authenticate() method finds a user by username and verifies the password against the hashed password in the database using BCrypt, on success the user details are returned with a JWT token. For more info on hashing and verifying passwords see .NET 5.0 - Hash and Verify Passwords with BCrypt.

The GetAll() method returns a list of all users in the system, and the GetById() method returns the user with the specified id.

using BCryptNet = BCrypt.Net.BCrypt;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Models.Users;

namespace WebApi.Services
{
    public interface IUserService
    {
        AuthenticateResponse Authenticate(AuthenticateRequest model);
        IEnumerable<User> GetAll();
        User GetById(int id);
    }

    public class UserService : IUserService
    {
        private DataContext _context;
        private IJwtUtils _jwtUtils;
        private readonly AppSettings _appSettings;

        public UserService(
            DataContext context,
            IJwtUtils jwtUtils,
            IOptions<AppSettings> appSettings)
        {
            _context = context;
            _jwtUtils = jwtUtils;
            _appSettings = appSettings.Value;
        }


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

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

            // authentication successful so generate jwt token
            var jwtToken = _jwtUtils.GenerateJwtToken(user);

            return new AuthenticateResponse(user, jwtToken);
        }

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

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

.NET App Settings JSON

Path: /appsettings.json

The appsettings.json file is the base configuration file in a .NET app that contains settings for all environments (e.g. Development, Production). You can override values for different environments by creating environment specific appsettings files (e.g. appsettings.Development.json, appsettings.Production.json).

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 unauthorised access to your application.

{
    "AppSettings": {
        "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
    },
    "Logging": {
        "LogLevel": {
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}
 

.NET Program Class with Main Method

Path: /Program.cs

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 method can be located anywhere in a project but is typically placed in the Program class.

A .NET web app is run within a host which handles app startup, lifetime management, web server configuration and more. A host is created and launched by calling Build().Run() on a host builder (an instance of the IHostBuilder interface). A generic host builder with pre-configured defaults is created with the CreateDefaultBuilder() convenience method provided by the static Host class (Microsoft.Extensions.Hosting.Host).

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

The x.UseStartup<Startup>() method specifies which startup class to use when building a host for the web app.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace WebApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(x => 
                {
                    x.UseStartup<Startup>();
                    x.UseUrls("http://localhost:4000");
                });
        }
    }
}
 

.NET Startup Class

Path: /Startup.cs

The Startup class configures the services available to the .NET Dependency Injection (DI) container in the ConfigureServices() method, and configures the .NET request pipeline for the application in the Configure() method. Both methods are called by the .NET runtime when the app starts, first ConfigureServices() followed by Configure().

The .NET host passes an IApplicationBuilder to the Configure() method, all DI services are also available to Configure() and can be added as parameters to the method (e.g. public void Configure(IApplicationBuilder app, DataContext context) { ... }). For more info on the startup class and both configure methods see https://docs.microsoft.com/aspnet/core/fundamentals/startup.

The createTestUsers() method is executed on startup to create a couple of users in the in-memory database for testing.

using BCryptNet = BCrypt.Net.BCrypt;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Services;
using System.Text.Json.Serialization;

namespace WebApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

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

        // add services to the DI container
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<DataContext>();
            services.AddCors();
            services.AddControllers().AddJsonOptions(x =>
            {
                // serialize enums as strings in api responses (e.g. Role)
                x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });

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

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

        // configure the HTTP request pipeline
        public void Configure(IApplicationBuilder app, DataContext context)
        {
            createTestUsers(context);

            app.UseRouting();

            // global cors policy
            app.UseCors(x => x
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

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

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

            app.UseEndpoints(x => x.MapControllers());
        }

        private void createTestUsers(DataContext context)
        {
            // add hardcoded test users to db on startup
            var testUsers = new List<User>
            { 
                new User { Id = 1, FirstName = "Admin", LastName = "User", Username = "admin", PasswordHash = BCryptNet.HashPassword("admin"), Role = Role.Admin },
                new User { Id = 2, FirstName = "Normal", LastName = "User", Username = "user", PasswordHash = BCryptNet.HashPassword("user"), Role = Role.User } 
            };
            context.Users.AddRange(testUsers);
            context.SaveChanges();
        }
    }
}
 

.NET MSBuild C# Project File (.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.

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.8" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.11.1" />
    </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