Appearance
Backend Technology Reference
Detailed reference for every significant technology used in the Microtec ERP .NET 8 backend.
Runtime & Language
.NET 8
Version: 8.0 LTS (Long-Term Support until November 2026)
Role: Primary backend runtime for all microservices
Key features used:
- Primary constructors (C# 12) for clean DI registration
IHostedService/BackgroundServicefor background workers- Native AOT considerations for future optimisation
- Minimal APIs (not used — controllers preferred for CQRS alignment)
System.Text.Jsonfor serialisation
csharp
// C# 12 primary constructor — used throughout the codebase
public class InvoiceHandler(
IUnitOfWork<AppDbContext> unitOfWork,
IClockService clock,
ILogger<InvoiceHandler> logger) : IRequestHandler<AddInvoiceCommand, int>
{
// unitOfWork, clock, logger available as fields
}CQRS & Mediator
MediatR
Version: 12.x
Role: In-process messaging bus; decouples controllers from business logic
Pattern: Command/Query Responsibility Segregation (CQRS)
Pipeline behaviors (applied in order):
| Behavior | Purpose |
|---|---|
LoggingBehavior | Log command name and execution time |
ValidationBehavior | Run FluentValidation before handler |
TransactionBehavior | Wrap commands in a DB transaction |
PerformanceBehavior | Warn when execution > 500 ms |
csharp
// Command — mutates state, returns a result
public record AddInvoiceCommand(string Number, decimal Total) : IRequest<int>;
// Query — read-only, never modifies state
public record GetAllInvoicesQuery(int Page, int Size) : IRequest<PaginatedList<InvoiceDto>>;
// Notification — fire-and-forget event
public record InvoiceCreatedNotification(int InvoiceId) : INotification;Input Validation
FluentValidation
Version: 11.x
Role: Validates all incoming DTOs and command objects
Integration: Registered as a MediatR pipeline behavior — validation runs before the handler
csharp
public class AddInvoiceCommandValidator : AbstractValidator<AddInvoiceCommand>
{
public AddInvoiceCommandValidator(IInvoiceRepository repo)
{
RuleFor(x => x.Number)
.NotEmpty().WithMessage("Invoice number is required")
.MaximumLength(50)
.MustAsync(async (number, ct) => !await repo.ExistsAsync(number, ct))
.WithMessage("Invoice number already exists");
RuleFor(x => x.Total)
.GreaterThan(0).WithMessage("Total must be positive");
}
}[INFO] Validators are discovered automatically via assembly scanning. No manual registration is needed if the validator is in the same assembly as the command.
Data Access
Entity Framework Core 8
Version: 8.x
Role: Primary ORM for all relational data access
Database: SQL Server 2022
Key conventions:
csharp
// Always use AsNoTracking() for read-only queries
var invoices = await context.Invoices
.AsNoTracking()
.Where(i => i.TenantId == tenantId && i.IsActive)
.OrderByDescending(i => i.CreatedAt)
.ToListAsync(cancellationToken);
// Global query filters (auto-applied for multi-tenancy)
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Invoice>()
.HasQueryFilter(i => i.TenantId == _tenantContextManager.TenantId);
}Repository pattern:
csharp
// IUnitOfWork wraps multiple repositories in a single transaction
public interface IUnitOfWork<TContext> where TContext : DbContext
{
IGenericRepository<T> Repository<T>() where T : BaseEntity;
Task<int> CompleteAsync(CancellationToken ct = default);
}Dapper
Version: 2.x
Role: Micro-ORM for complex reporting queries where EF generates inefficient SQL
Used in: Reporting.Apis, export queries with many JOINs
Background Processing
Hangfire
Version: 1.8.x
Role: Scheduled and deferred background jobs
Storage: SQL Server (Microtec_Hangfire database)
Used for: Invoice processing, email sending, data exports, ZATCA submission
csharp
// Fire-and-forget job
BackgroundJob.Enqueue<IInvoiceProcessor>(p => p.ProcessAsync(invoiceId));
// Scheduled job
BackgroundJob.Schedule<IReportGenerator>(g => g.GenerateDailyReport(), TimeSpan.FromHours(1));
// Recurring job (CRON)
RecurringJob.AddOrUpdate<ICleanupService>("cleanup-old-sessions",
s => s.CleanupAsync(), Cron.Daily(2)); // 02:00 dailyAPI Gateway
YARP (Yet Another Reverse Proxy)
Version: 2.x
Role: Reverse proxy in Gateway.API — routes external requests to internal services
Features used:
- Path-based routing to different Container Apps
- JWT token forwarding
- Load balancing across service replicas
- Request/response transformation
json
// ocelot.dev.json — route configuration
{
"Routes": [{
"DownstreamPathTemplate": "/api/{everything}",
"DownstreamHostAndPorts": [{"Host": "mic-erp-be-dev-apps-portal", "Port": 8080}],
"UpstreamPathTemplate": "/apps-portal/{everything}",
"AuthenticationOptions": {"AuthenticationProviderKey": "Bearer"}
}]
}Messaging
MassTransit
Version: 8.x
Role: Messaging abstraction layer — decouples message producers from transport
Transports:
- Local dev: RabbitMQ (Docker)
- Cloud (all envs): Azure Service Bus Standard tier
csharp
// Producer — publish an event
public class InvoiceHandler(IPublishEndpoint publishEndpoint)
{
await publishEndpoint.Publish(new InvoiceCreatedEvent { InvoiceId = invoice.Id });
}
// Consumer — handle an event
public class SendInvoiceEmailConsumer : IConsumer<InvoiceCreatedEvent>
{
public async Task Consume(ConsumeContext<InvoiceCreatedEvent> context)
{
await emailService.SendInvoiceEmailAsync(context.Message.InvoiceId);
}
}Resilience
Polly
Version: 8.x
Role: Retry policies, circuit breakers, and timeout policies for HTTP calls
Used in: All typed HTTP clients (IAccountingPublicApi, INotificationApi, etc.)
csharp
services.AddHttpClient<IAccountingPublicApi, AccountingPublicApi>()
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))))
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));Object Mapping
AutoMapper
Version: 12.x
Role: Maps between domain entities and DTOs
Convention: Profiles are in {Module}.Application/Mappings/
csharp
public class InvoiceMappingProfile : Profile
{
public InvoiceMappingProfile()
{
CreateMap<Invoice, InvoiceDto>()
.ForMember(d => d.CustomerName, o => o.MapFrom(s => s.Customer.Name));
CreateMap<AddInvoiceCommand, Invoice>()
.ForMember(d => d.CreatedAt, o => o.Ignore()); // Set by handler
}
}Logging
Serilog
Version: 3.x
Role: Structured logging with multiple sinks
Sinks configured by Microtec.Web.Hosting:
- Console (development)
- Seq (via OTLP)
- Application Insights (cloud environments)
csharp
// Structured log — properties are searchable in Seq
Log.Information("Invoice {InvoiceId} created for tenant {TenantId} in {ElapsedMs}ms",
invoice.Id, tenantId, stopwatch.ElapsedMilliseconds);
// Never use string interpolation for log messages
// Wrong: Log.Information($"Invoice {invoice.Id} created"); // Not structured!Observability
OpenTelemetry .NET
Version: 1.x
Role: Distributed tracing, metrics, and log correlation
Exporters: OTLP → Seq (dev), OTLP → Azure Monitor (cloud)
Instrumentation auto-added by Microtec.Web.Hosting:
- ASP.NET Core HTTP traces
- EF Core SQL traces
- HttpClient traces
- MassTransit message traces
Document Generation
QuestPDF
Version: 2024.x
Role: PDF report generation — invoices, financial reports, exports
Package: Microtec.Reporting
csharp
Document.Create(container =>
{
container.Page(page =>
{
page.Content().Column(col =>
{
col.Item().Text(invoice.Number).FontSize(16).Bold();
col.Item().Table(table => { /* ... */ });
});
});
}).GeneratePdf(stream);ClosedXML
Version: 0.102.x
Role: Excel (.xlsx) report generation
Package: Microtec.Reporting
Security
HtmlSanitizer
Version: 8.x
Role: Sanitise HTML input to prevent XSS attacks
Applied to: All fields that accept rich text (notes, descriptions, email templates)
csharp
var sanitizer = new HtmlSanitizer();
var safeHtml = sanitizer.Sanitize(userInput); // Strips dangerous tags/attributesTesting
| Library | Role |
|---|---|
| xUnit 2.x | Test runner and assertion framework |
| Moq 4.x | Mock creation for unit tests |
| FluentAssertions 6.x | Readable assertion syntax |
| Testcontainers 3.x | Spin up real SQL Server/Redis containers for integration tests |
| Bogus | Generate realistic fake test data |
| Microsoft.AspNetCore.Mvc.Testing | In-process WebApplicationFactory for API tests |
csharp
// Integration test using Testcontainers
public class InvoiceApiTests : IAsyncLifetime
{
private readonly MsSqlContainer _sql = new MsSqlBuilder().Build();
public async Task InitializeAsync() => await _sql.StartAsync();
public async Task DisposeAsync() => await _sql.StopAsync();
[Fact]
public async Task AddInvoice_ReturnsCreatedId()
{
// Arrange
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(b => b.UseSetting(
"ConnectionStrings:Default", _sql.GetConnectionString()));
// Act + Assert
var client = factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/invoices", new { Number = "INV-001" });
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}