AspNano.WebApi is a top-level project. In this project you will find the program.cs class which is the entry point of the application. This is the expanded view of the web api project.
Separating the application into distinct layers is what’s known as Clean Architecture. Along with many other benefits, it keeps the application organized and modular. In basic Monolithic applications, the domain, controllers, and services all reside in one project, separated by folders and namespaces.
Unlike the other projects which are class libraries, the Web Api project is a console application. In other words, the Web API project is the actual application with http controllers. The other projects become part of it when the program compiles.
The following sections go into more detail on each component in the Web Api project.
The program.cs class is the entry point of the application. In this class, the application builder is created and services are registered.
The code for configuring and registering services has been moved to extension methods found in the ServiceCollectionExtensions.cs class. Where possible, values for configuration options are passed in from appsettings.json.
When developing, the React application will be running on a different localhost. Therefore, the solution is set up to allow any origin with CORS by default to prevent request blocking.
appsettings.json is an external file with key value pairs for application settings. Keeping these settings managed externally makes them easy to change and allows the application to adapt to different deployments.
- Connection string
- Kestrel Host
- JWT, CORS, and logging settings
- Test credentials for the mail and image services.
Cloudinary is an image and file management platform with a .Net SDK that makes image and file handling easy. You can use the provided key and secret for testing or replace with your own credentials.
Ethereal is a free mail service for testing. Since these credentials only last a few days, you will need to replace the keys with your own.
This class contains extension methods for configuring and registering services. If you are unfamiliar with extensions methods you can read about them here. These extension methods are used to move the service registration and configuration code out of the program.cs class for better organization.
This class contains four main regions:
- Add Controllers and Services
- Registering DB Context Service
- Setting up Identity Configuration
- JWT Settings
Add Controllers and Services
In this region:
- Mapping API endpoints
- Setting API controllers with default authorization policy.
- Auto registration of all fluent validation classes in assemblies (class libraries) containing marker interface IRequestValidator
- Configuring AutoMapper with MappingProfiles found in the infrastructure project
- Configuring Cloudinary and Mail Settings from appsettings.json
- Dynamic Service registrations with AddServices extension method
Additional infrastructure services should be added here if they require configuration. Otherwise, by inheriting from ITransientService or IScopedServices interfaces.
The AddServices extension method from the from DynamicServiceRegistrationExtensions auto register transient and scoped services.
Registering DB Context
In this region, three database contexts are registered:
- BaseDbContext, a main context which covers Identity (inherits from IdentityDB), and tenants
- ApplicationDbContext, a main context which covers every other application table
- TenantDbContext, an auxiliary ‘read-only’ context which knows only about the tenant table
By default, the contexts use SQL Server. To use a different type of database like MySQL, MariaDB, or any other database supported by Entity Framework, import EntityFramework.Extensions from NuGet and change the option to another provider.
If you only plan on having one shared database, then BaseDbContext and ApplicationDbContext could be merged into one. However, the auxiliary TenantDbContext is always needed is because middleware needs to look up tenant info in the database. That tenant info is then used to configure the main context per request. Explained further in middleware.
Setting up Identity Configuration
This region configures the ASP Identity framework. Identity is managed by the BaseDbContext.
The AddIdentity service registration requires two parameters:
- IdentityRole (for role-based access)
Password requirements, are set low by default and can be changed here as needed.
In this region, the application is configured to use JWT tokens for authentication.
The actual issuing of tokens is handled by the TokenService in the infrastructure project.
The AddServices method in the DynamicServiceRegistrationExtensions class is a helper method that auto registers any services which inherit from the ITransientService interface or IScopedSerivce interface. For example, the Venue service in the Application project will be registered automatically as a transient service because it’s interface, IVenueService, derives from the ‘marker’ interface ITransient Service.
When creating new services, inherit from ITransient or IScopedService to include them in the DynamicServiceRegistrationExtensions process.
This extension method creates a DbInitializer class and runs the SeedTenantAminAndRoles method. The DbInitializer class seeds the database with roles and a root tenant/admin user if no root tenant is found.
As of version 1.5, SeedDatabase is no longer called called directly in program.cs. Instead its run as part of the AddAndMigrateTenantDatabases method found in Infrastructure/Persistence/Extensions/MultipleDatabasesExtensions.
Middleware in .Net Core apps gets run per each request (before reaching a controller). Middleware can be chained together to form a pipeline for performing conditional logic on incoming requests. This makes middleware the perfect solution for handling tenant resolution.
There is only one middleware class in the solution, the TenantResolver. The TenantResolver middleware is responsible for extracting the TenantId from the incoming requests.
The TenantResolver makes use of one service, the CurrentTenantUserService, which is a scoped service in the WebApi project.
Services are injected differently in middleware than in regular classes. In middleware, they are put into the InvokeAsync method, not the constructor.
If the request is authenticated, the HttpContext will contain a token. And this token will contain a value for tenant and user. The TenantResolver will set the TenantId and UserId values in the CurrentTenantUserService.
If the request is not authenticated, there will not be a token in the HttpContext. In this case, the tenant id will be obtained from the subdomain (optional) or from the request header’s tenant value.
GetSubDomain is a helper function that extracts a subdomain string based on url segment count. Accessing the app via localhost:7250, will return a null value. Accessing the app via beta.localhost:7250, would return string ‘beta’.
In production, setting up subdomains will vary depending on hosting environments.
In the Nano ASP.NET Boilerplate, the only non-authenticated requests are login (get-token) and forgot password. All other API calls are authenticated, which means that the tenantId will come from a token.
In the postman collection, the two non-authenticated requests, get-token and forgot-password, contain a tenant value in the Header. All other requests contain a bearer token for authentication.
Usually, services reside in the Infrastructure or Application projects. In the Nano Boilerplate there is one exception and that is the CurrentTenantUserServices which is used to read the tenantId, connectionString, and userId values in the JWT token or request header. This service has access to the TenantDbContext and can read data from the tenant table.
The CurrentTenantUserService is a scoped service which resides in the WebApi project. Its usage is invoked in the tenant resolver middleware per each request.
It contains one method, the SetTenantUser method, which looks up the tenant information in the database via tenantDbContext. If an active tenant is found, the TenantId and ConnectionString properties are set. If a token is present, the UserId will also be set.
Since CurrentTenantUserService is a scoped service, the UserId, ConnectionString, and TenantId values will be available throughout the entire request lifecycle.
The connection string property is only used in multiple tenant database scenarios and can be removed if planning on using a single shared database. In cases where the connection string is null, the default connection string is used.
The controllers are http RESTful endpoints which are mapped with the [http] attributes Get, Post, Put, Delete.
Access is controlled at this level by providing an [Authorize] attribute along with a list of roles permitted. Endpoints containing the [AllowAnonymous] attribute such as the tokens (login), tenant list (optional), forgot password, and fallback controller allow access for non authenticated requests.
The controllers don’t contain a lot of code themselves but rather make use of Dependency Injection to consume services found in the Application and Infrastructure layers.
There are 5 controllers in the solution:
The tokens controller takes anonymous requests and returns a token if successful. It makes use of the token service in the infrastructure project.
The identity controller has endpoints for profile and user management. It makes use of the identity service in the infrastructure project.
The tenants controller has endpoints for managing tenants. It makes use of the tenant service in the infrastructure project.
The venues controller has endpoints for managing venues, which is a sample business entity. It makes use of the venue service in the application project. New controllers will likely follow this pattern.
The fallback controller is a controller that loads the React application static files (in the wwwroot folder) when a user first visits.
This folder is for static files, like the react application.