Adding a new Service
From the root of the application, run the following command:
nexus add service "servicename"
e.g.
nexus add service "company"
Entities
To add a new entity, create a new class in the Entities folder and have it extend the AuditableNexusEntityBase class:
public class Company : AuditableNexusEntityBase
{
public Company(string name)
{
Name = name;
}
public string Name { get; private set; }
public virtual List<Tag> Tags { get; set; } = new ();
}
The AuditableNexusEntityBase class contains fields used to manage audit information including when the entity was
created/modifed and by which user. It also contains an Identifier field.
Database Access
Entity Configuration
For each added entity, you might want to create a configuration file that tells entity framework how to setup the
fields and the table. This can be done by adding a new Configuration class in the Data/Configuration folder:
public class CompanyConfiguration : IEntityTypeConfiguration<Company>
{
/// <summary>
/// Configures the entity type and its relationships.
/// </summary>
/// <param name="builder">The builder used to configure the entity type.</param>
public void Configure(EntityTypeBuilder<Company> builder)
{
// Configures the maximum length and requirement of the company name.
builder.Property(c => c.Name)
.HasMaxLength(255)
.IsRequired();
// Configures the table name for the company entity.
builder.ToTable("Company");
// Configures a many-to-many relationship between the company and tag entities.
builder
.HasMany<Tag>(c => c.Tags)
.WithMany(t => t.Companies)
.UsingEntity("CompanyTag", typeof(Dictionary<string, object>),
right => right.HasOne(typeof(Tag)).WithMany().HasForeignKey("TagId"),
left => left.HasOne(typeof(Company)).WithMany().HasForeignKey("CompanyId"));
}
}
ApplicationDbContext
The entities will need to be added to the ApplicationDbContext class:
public class ApplicationDbContext : AuditableDbContext
{
// Rest of the code
public DbSet<Company> Companies => Set<Company>();
// Rest of the code
}
Repositories
Repositories provide a standard way to fetch data from the Database. The EfNexusRepository provides a set of methods for this. It can be extended to allow for custom functionality on top of the provided one:
[NexusService(NexusServiceLifeTime.Scoped)]
public class CompanyRepository : EfNexusRepository<Company>
{
public CompanyRepository(ApplicationDbContext context) : base(context)
{
}
public async Task<List<Company>> AllCompaniesWithTagsAsync()
{
return await DbSet.Include(x => x.Tags).ToListAsync();
}
public async Task<bool> ExistsWithNameAsync(string name, CancellationToken cancellationToken = default)
{
return await DbSet.AnyAsync(x => x.Name == name, cancellationToken);
}
public async Task<Company?> GetByNameAsync(string name, CancellationToken cancellationToken = default)
{
return await DbSet.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
}
}
UnitOfWork
In case you need transactional support, you can also add the Repository to the UnitOfWork:
[NexusService(NexusServiceLifeTime.Scoped)]
public class UnitOfWork : UnitOfWorkBase
{
public UnitOfWork(ApplicationDbContext context,
CompanyRepository companyRepository)
: base(context)
{
Companies = companyRepository;
}
public CompanyRepository Companies { get; }
}
Migrations
To add migrations, run the following commands from the csproj directory:
dotnet ef migrations add "Init" --output-dir .\Data\Migrations
dotnet ef database update
The first command adds a new migration called "Init" and stores the state of the ApplicationDbContext in the
Data/Migrations folder.
The second commands applies the migrations to the database.
Services
- Services contain business logic for the service
- The interfaces for the services are added to the
Abstractionsfolder - The implementations in the
Servicesfolder - Services can communicate with the database using the repositories and UnitOfWork classes
- Services return DTOs which are stored in the
DTOfolder
Api Controllers and Request/Response Models
- Controllers consume the entity services and provide REST functionality
- The endpoints accept Requests. These are special kinds of DTOs and are stored in the
Modelfolder. They follow the name convention<Entity>[Action]RequestModel. e.g.CompanyCreateRequestModel. - The endpoints return Responses. These are special kinds of DTOs and are stored in the
Modelfolder. They follow the name convention<Entity>[Adjective]ResponseModel. e.g.CompanyResponseModel.
Request Model Validation
The services are setup to use FluentValidation to ensure the requests are valid. The validators extend the
AbstractValidator<T> class provided by FluentValidation:
public class CompanyRequestModelValidator : AbstractValidator<CompanyRequestModel>
{
public CompanyRequestModelValidator()
{
RuleFor(c => c.Name)
.NotNull()
.MinimumLength(5)
.MaximumLength(255);
}
}
The validators are stored in the Model/Validation folder.
Mapping Profiles
To translate between Entities, DTOs, and models, the service is setup to use Automapper. Mapping profiles are stored in
the Mapping folder:
public class CompanyProfile : Profile
{
public CompanyProfile()
{
CreateMap<Company, CompanySummaryDto>();
CreateMap<Company, CompanyDto>();
CreateMap<CompanySummaryDto, Company>();
CreateMap<CompanyDto, CompanyResponseModel>();
}
}
Telemetry/Instrumentation
Instrumentation is useful to generate application metrics useful for your application. e.g. How many times a day do
users search for a particular term. To do this, create a custom meter and increment it in your application. An example
for this can be found in the Company Api in the CompanyInstrumentation class in the Telemetry folder. Its use can
be reviewed in the CompanyController.
Customizing Bootstrapper / Dependency Injection
Ensure your services are registered in the DI container by reading the guide on setting up the framework here