Skip to content

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 / BackgroundService for background workers
  • Native AOT considerations for future optimisation
  • Minimal APIs (not used — controllers preferred for CQRS alignment)
  • System.Text.Json for 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):

BehaviorPurpose
LoggingBehaviorLog command name and execution time
ValidationBehaviorRun FluentValidation before handler
TransactionBehaviorWrap commands in a DB transaction
PerformanceBehaviorWarn 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 daily

API 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/attributes

Testing

LibraryRole
xUnit 2.xTest runner and assertion framework
Moq 4.xMock creation for unit tests
FluentAssertions 6.xReadable assertion syntax
Testcontainers 3.xSpin up real SQL Server/Redis containers for integration tests
BogusGenerate realistic fake test data
Microsoft.AspNetCore.Mvc.TestingIn-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);
    }
}

Internal Documentation — Microtec Platform Team