The infrastructure project is a class library which contains services for generic functionality like persistence, user management, and authorization.
The infrastructure project contains the following sections, in the order of usage as a request comes in:
- Auth (Token Service)
Authorization (Token Service)
Within the Auth folder is the Token Service, which is responsible for authenticating users and providing a JWT token. JWT tokens provide the basis for stateless authentication.
The Token Service has one public method, GetTokenAsync. The GetTokenAsync method checks if the user exists, is active, and has a valid password. GenerateJWTToken is a private method that issues and signs the token.
When the userManager is checking if the user exists, the query is already filtered for the tenant (see persistence for more info). With this setup, tenant emails do not need to be unique across tenants.
The JWT token contains the values (claims) for the tenant Id, user Id, and roles the user belong to. The duration of the JWT token is dictated by the value set in appsettings.json, which by default is 120 minutes.
This service uses methods from the UserManager and SignInManager, which are classes provided by Microsoft ASP Identity framework. This service is only used by the solution in the tokens controller for signing in users.
The persistence folder contains everything related to Entity Framework and data persistence. It contains 5 sub folders:
The context classes are used by Entity Framework to map the database, and there can be more than one. There are three contexts in the Nano Boilerplate:
- TenantDbContext (reference-only)
In a shared tenant database scenario, the BaseDbContext and ApplicationDbContext manage different tables in the same database and could actually be merged into one. In a multiple tenant database scenerio, the BaseDbContext manages tables that are only found in the central shared database, while the ApplicationDbContext manages tables that are found in all databases.
In code first development, the database and schema are dictated by the entity classes.
BaseDbContext (main context)
The BaseDbContext is the main context for the application responsible for the Tenants table, Identity, and any tables added related to global configuration. Entity Framework migrations are run against this context. In scenarios where there are multiple tenant databases, any entities managed by BaseDbContext will not be applied on individual tenant databases, only the central shared database.
The ASP Identity service is configured using this context. BaseDbContext inherits from IdentityDbContext<ApplicationUser>.
When creating migrations:
- Specify the infrastructure project in the Package Manager Console
- Use the add-migration command with the -Context BaseDbContext switch and the output directory switch -o Persistence/Migrations/BaseDb Base-MigrationName
- Running the application will apply any pending migrations, or use the update-database command with the -Context BaseDbContext switch
add-migration -Context BaseDbContext -o Persistence/Migrations/BaseDb Base-NewMigration
update-database -Context BaseDbContext
Because migrations are run programmatically, and in some cases, with dynamic connection strings, the Entity Framework Design-Time tools (the migration scaffolding tools) need some help in determining which connection string to use. For that reason, its necessary to include a class that inherits from IDesignTimeDbContextFactory, and pass the default connection string as an option. The BaseDbContextFactory does that and uses the default connection string from appsettings.json.
In BaseDbContext there are two override methods (see the next section for details):
- OnModelCreating – applies query filters to isolate Application Users and soft delete
- SaveChangesAsync – performs automation on saving changes
The BaseDBContext is only concerned with the central shared database and always uses the default connection string. Therefore it does not need an OnConfiguring method like ApplicationDbContext.
ApplicationDbContext (main context)
The ApplicationDbContext is the main context for the application responsible for everything else. Entity Framework migrations are run against this context. It contains DbSets for each table in the database which are related to the application, like Venues. When new entities are added to the application, a new DbSet<T> should be added here. In a multiple tenant database scenario, migrations on these entities will be run on every tenant database, in addition to the main one.
When creating migrations:
- Specify the infrastructure project in the Package Manager Console
- Use the add-migration command with the -Context ApplicationDbContext switch and the output directory switch -o Persistence/Migrations/AppDb App-MigrationName
- Running the application will apply any pending migrations, or use the update-database command with the -Context ApplicationDbContext switch
add-migration -Context ApplicationDbContext -o Persistence/Migrations/AppDb App-NewMigration
update-database -Context ApplicationDbContext
In ApplicationDbContext there are three override methods:
- OnModelCreating – applies query filters to isolate tenant data, and soft delete
- OnConfiguring – configures the connection string per each request
- SaveChangesAsync – performs automation on saving changes
The OnModelCreating applies query filters using the current tenant Id, to any entity which implements the IMustHaveTenant interface. The tenant Id is obtained via the CurrentTenantUserService which is injected into ApplicationDbContext.
Soft deletion uses a query filter in a similar way. Any rows in tables with ISoftDelete will not be included in the results if IsDeleted is true.
Any tables that do not implement IMustHaveTenant will be visible across all tenants, which could be useful for static data like zip codes, country lists, etc. Keep in mind though that if using per tenant databases, only the data for the current tenant will be accessable.
In the SaveChangesAsync override, automation is performed every time data is saved. The following actions take place in the TenantAndAuditFields extension method.
- Each time a user saves data for an entity marked as IMustHaveTenant, the currentTenantId is saved to the entity’s TenantId column.
- Each time a user saves data for an entity marked as IAuditableEntity, the createdOn and ModifiedOn fields have UTC dates saved along with the currentUserId.
- Each time a user deletes data for an entity marked ISoftDelete, the entity state change is intercepted and saved as Modified (as opposed to Delete).
The TenantDbContext has a much smaller role compared to the BaseDbContext and ApplicationDbContext. However, it’s necessary have a separate context for tenant lookup to avoid circular logic errors when the initial search for the tenant is performed in the middleware.
The TenantDbContext is only aware of the Tenants table and contains no query filers or authentication. An incoming always request begins with a tenant Id set in the CurrentTenantUserService (which uses the TenantDbContext), so that the BaseDbContext and ApplicationDbContext can build a model with query filtering per tenant.
Entity Framework migrations are performed on the BaseDbContext and ApplicationDbContext, not the TenantDbContext. New entities should be added as DbSets<> to the ApplicationDbContext only, and to BaseDbContext in cases where the data should be maintained only in the central shared database.
If you never plan to use multiple tenant database, and just want to manage a single shared database, then ApplicationDbContext and BaseDbContext can be combined into one context. The BaseDbContextFactory class, ConnectionString tenant property, and OnConfiguring override can all be removed for simplicity. Or just leave the connection string property null for all tenants. Any tenant with a null connection string will use the same default database.
There are four extension methods in the persistence area,
- ApplyIdentityConfiguration – renames the default Asp Identity table names in the database.
- AppendGlobalQueryFilter – helper method to find all entities that implement a given interface type.
This extension contains one class, SeedStaticData. There are multiple ways of seed databases in .Net 6. This example is using model seed data, which is useful for static data like zip codes, country lists, etc. but shouldn’t be used for “data that requires calls to external API, such as ASP.NET Core Identity roles and users creation.” Model seed data is managed by entity framework migrations. For tenants, users, and roles, custom initialization logic is used (DbInitializer).
This class contains one method AddAndMigrateTenantDatabases which is invoked during the app startup (see ServiceCollectionExtensions) in either the WebApi or RazorApp project.
This method first applies any pending migrations to the BaseDbContext. Then it checks to see if there is a root tenant, and in the case there isn’t, it will run the Initializer and seed a root tenant with admin and roles.
Next it gets a list of tenants using the TenantDbContext, which is just a read-only context, and loops through this list, obtaining a tenant-specific connection string if there is one. It will apply any pending migrations to ApplicationDbContext to the default shared database, and to any tenant specific ones.
The DbInitializer class contains one method, SeedTenantAdminAndRoles which is invoked during the AddAndMigrateTenantDatabases method found in the MultipleDatabaseExtension. This is custom initialization logic for seeding the database with a root tenant, root tenant admin user, and roles if they are not found. This seed method is called within the AddAndMigrateTenantDatabases method right before the app is run, and will trigger on the initial deployment.
Entity Framework uses migration classes to scaffold the database and stores them in a folder named Migrations by convention. The solution ships with an initial migration for each context already created. As outlined in the setup guide, when running Nano ASP.NET solution for the first time, running the app will automatically apply and pending migrations, or you can execute the update-database commands
update-database -Context BaseDbContext update-database -Context ApplicationDbContext
The repository class is a layer of abstraction that sits between the ApplicationDbContext and the application services. It’s used whenever data is stored or retrieved by an application service. This is a generic repository which can be used by all entities managed with ApplicationDbContext.
The repository class contains methods which mimic the functionality Entity Framework provides while also extended functionality like domain object mapping and pagination. When used in conjunction with the Specification classes, any type of query can be constructed. With the specification pattern, the application is decoupled from any specific persistence technology. The Nano boilerplate implements the Ardalis Specification Nuget package with Entity Framework Core in the infrastructure project. For more information regarding Ardalis Specification and how to construct specification classes, you can read the documentation on their site.
The identity service contains methods for managing users in the system and for users to manage their profiles, reset passwords, etc.
- The service contains two methods for get users – a full list or paginated result. The users list in the react client handles pagination client side with React Table, and therefore uses the full list.
- Registering a new user requires a valid email and password as well as a role specified.
- Updating your profile allows for uploading an image. To do so, this method uses functionality provided via the CloudinaryService (image/file handling platform).
- Updating a user contains code to prevent setting yourself as inactive, and for reassigning roles.
- The forgot password method uses functionality provided by the MailService.
The TenantManagementService contains methods for performing actions on tenant entities.
- GetAllTenants returns a full list, which can be paginated on the client.
- SaveTenant will create a new tenant with a default admin-level user. In cases where a connection string is provided, the tenant will use a separate database. The new database will carry the name of the tenant as a suffix and any pending ApplicationDbContext migrations will be run.
- UpdateTenant allows for changing the name and active status of a given tenant, excluding the root tenant.
A new database will be created per each tenant if a connection string is provided, and this will work fine in development. For a real deployment on Azure or AWS, provider-specific code should be added to programmatically create new databases and subdomains.
When dealing with assets like files and images, the best approach of how to handle these will always vary depending on the type of application being built. One approach is to manage and store the assets locally within the filesystem. Another option is to use a 3rd party platform which provides functionality with an SDK.
Image handling in this solution uses the Cloudinary .Net SDK. Cloudinary is a 3rd party platform specializing in image processing, and provides advanced functions like cropping photos with AI, programmatically resizing images, etc. The documentation for Cloudinary can be found here.
There are only two methods in the CloudinaryService:
- Add Image takes a request sent as form-data (notice the difference in Postman and React)
- Delete Image removes an image by url
Mailing is handled with the IMailService interface, which implements MailService. The MailService class makes use of MailKit, which is an email framework for .Net available as a NuGet package. Per its documentation, its goal is ‘to provide the .NET world with robust, fully featured and RFC-compliant SMTP, POP3, and IMAP client implementations.’
The mailing service is used in the ForgotPassword method of the IdentityService.
The MappingProfiles class is the mapping configuration for Auto Mapper. Auto Mapper is a useful NuGet package which automates the repetitive action of assigning of values to DTOs.
In summary, CreateMap defines ‘map to’ and ‘map-from’ between and entity and a DTO (or vice-versa) and will find the fields that correspond as long as their names are the same. To map fields with different names, additional mapping configuration is required.
In the Nano ASP.NET boilerplate, DTO classes (request and response objects) are kept in DTO folders per each service.
The Auto Mapper service is registered in the ServiceCollectionsExtensions class (web api project) with the MappingProfiles class passed in as configuration.
The mapper is used by the repository class and anywhere else where mapping is needed.
The utility folder contains one class:
- IRequestValidator is a blank marker class which allows Fluent Validation to be aware of the infrastructure project when it gets configured in the ServiceCollectionsExtensions class.