Skip to content

Backend Developer Onboarding

A practical guide to becoming productive on the Microtec ERP .NET 8 backend.

Prerequisites: Complete Day 1 Checklist first
Stack: .NET 8, MediatR, EF Core 8, FluentValidation, MassTransit, Serilog


Solution Structure

Two solutions sit under Platforms/:

Platforms/
├── Microtec.Platforms.sln         ← ERP + Infrastructure (~30 projects)
├── Microtec.BusinessOwner.sln     ← Tenant admin portal
└── Src/
    ├── AppsPortal/                ← ERP modules
    │   └── Accounting/            ← Most complete reference module
    │       ├── AppsPortal.Apis/           # REST controllers
    │       ├── AppsPortal.Application/    # CQRS commands/queries
    │       ├── AppsPortal.Domain/         # Entities, value objects
    │       └── AppsPortal.Infrastructure/ # EF Core, repositories
    ├── BusinessOwners/            ← Multi-tenant admin portal
    ├── InfrastructureServices/    ← Cross-cutting microservices
    │   ├── Attachment/
    │   ├── Notification/
    │   ├── Workflow/
    │   ├── Import/
    │   ├── Zatca/                 # Saudi e-invoicing
    │   └── Reporting/
    ├── Shared/                    ← Becomes NuGet packages
    │   ├── Shared.Core/           # → Microtec.Domain
    │   ├── Shared.Infrastructure/ # → Microtec.Persistence
    │   └── Shared.Web/            # → Microtec.Web.Core / Microtec.Web.Hosting
    ├── Auth/                      ← Auth middleware (packaged)
    ├── Gateway/                   ← YARP reverse proxy
    └── Website/                   ← Marketing website

[INFO] Start by reading the Accounting module — it is the most complete and follows all conventions correctly. Use it as your reference implementation.


Key Patterns

CQRS with MediatR

Every feature is implemented as a command or query. Controllers are thin — they only dispatch to MediatR.

Command flow:
  Controller → Send(AddInvoiceCommand) → AddInvoiceCommandHandler → Result

Query flow:
  Controller → Send(GetAllInvoicesQuery) → GetAllInvoicesQueryHandler → PaginatedList<InvoiceDto>

Naming convention (strict — must follow exactly):

OperationCommand/Query Name
CreateAdd{Entity}Command
UpdateEdit{Entity}Command
DeleteDelete{Entity}Command
Get by IDGetById{Entity}Query
Get paginated listGetAll{Entity}Query
Get dropdownGetDropdown{Entity}Query
ExportExport{Entity}Query

File locations:

AppsPortal.Application/
└── Invoice/
    ├── Commands/
    │   ├── Add/
    │   │   ├── AddInvoiceCommand.cs
    │   │   ├── AddInvoiceCommandHandler.cs
    │   │   └── AddInvoiceCommandValidator.cs
    │   └── Edit/
    │       └── ...
    └── Queries/
        ├── GetAll/
        │   ├── GetAllInvoicesQuery.cs
        │   └── GetAllInvoicesQueryHandler.cs
        └── GetById/
            └── ...

Command handler example

csharp
public record AddInvoiceCommand(string Number, decimal Total) : IRequest<int>;

public class AddInvoiceCommandHandler(IUnitOfWork<AppDbContext> unitOfWork, IClockService clock)
    : IRequestHandler<AddInvoiceCommand, int>
{
    public async Task<int> Handle(AddInvoiceCommand request, CancellationToken cancellationToken)
    {
        var invoice = new Invoice
        {
            Number = request.Number,
            Total  = request.Total,
            CreatedAt = clock.Now    // Never use DateTime.Now directly
        };

        await unitOfWork.Repository<Invoice>().AddAsync(invoice, cancellationToken);
        await unitOfWork.CompleteAsync(cancellationToken);
        return invoice.Id;
    }
}

FluentValidation

csharp
public class AddInvoiceCommandValidator : AbstractValidator<AddInvoiceCommand>
{
    public AddInvoiceCommandValidator()
    {
        RuleFor(x => x.Number)
            .NotEmpty()
            .MaximumLength(50);

        RuleFor(x => x.Total)
            .GreaterThan(0);
    }
}

Your First Feature

Follow this exact sequence for any new feature:

  1. Create domain entity in {Module}.Domain/Entities/
  2. Add EF Core mapping in {Module}.Infrastructure/Persistence/Configurations/
  3. Create migration via dotnet ef migrations add {MigrationName}
  4. Write command/query in {Module}.Application/{Feature}/Commands/ or /Queries/
  5. Write validator alongside the command
  6. Write handler — inject IUnitOfWork and IClockService
  7. Write controller action — thin, just dispatch to MediatR
  8. Write tests — unit tests for handler, integration tests for API endpoint

Running a specific test

bash
cd Platforms

# Run a single test class
dotnet test --filter "FullyQualifiedName~AddInvoiceCommandHandlerTests"

# Run all tests in a project
dotnet test Src/AppsPortal/Accounting/AppsPortal.Tests/AppsPortal.Tests.csproj

# Run with verbose output
dotnet test --logger "console;verbosity=detailed"

Key Services and Patterns

Multi-tenancy

Every API request carries a tenant context. The platform resolves the tenant from the JWT token:

csharp
// Inject ITenantContextManager to access current tenant info
public class MyHandler(ITenantContextManager tenantContext)
{
    var tenantId = tenantContext.TenantId;
    var dbName   = tenantContext.DatabaseName;
}

Database-per-tenant: Each tenant has their own SQL database. IUnitOfWork<TContext> automatically connects to the correct database for the current request.

Read-only queries — always use AsNoTracking()

csharp
// Correct — no EF change tracking overhead for reads
var invoices = await unitOfWork.Repository<Invoice>()
    .GetAllAsync(query => query.AsNoTracking().Where(i => i.TenantId == tenantId));

// Wrong — EF tracks all returned entities unnecessarily
var invoices = await dbContext.Invoices.ToListAsync();

Service registration — marker interfaces

csharp
// Scoped (per HTTP request) — most services
public class InvoiceService : IInvoiceService, IScopedService { }

// Transient (new instance per injection)
public class PdfBuilder : IPdfBuilder, ITransientService { }

// Singleton (one instance for app lifetime)
public class TenantCache : ITenantCache, ISingletonService { }

AddSharedWeb() automatically discovers and registers all classes implementing these markers via assembly scanning.

Inter-service communication

Use typed HTTP clients from Microtec.PublicApi.* packages:

csharp
public class MyHandler(IAccountingPublicApi accountingApi)
{
    var balance = await accountingApi.GetAccountBalanceAsync(accountId);
}

IClockService — always use instead of DateTime

csharp
// Wrong
var now = DateTime.Now;
var utcNow = DateTime.UtcNow;

// Correct — testable and consistent
public class MyHandler(IClockService clock)
{
    var now = clock.Now;      // Returns DateTimeOffset in configured timezone
}

NuGet Packages Reference

PackagePurposeAdd to
Microtec.Web.HostingFull bootstrap (auth + EF + MediatR + logging)*.Apis projects
Microtec.Web.CoreAuth middleware onlyAdvanced use
Microtec.DomainBase entity, audit interfaces*.Domain projects
Microtec.PersistenceGeneric repository, UoW*.Infrastructure projects
Microtec.ContractsCQRS interfaces onlyShared library projects
Microtec.ReportingPDF (QuestPDF) + Excel (ClosedXML)Reporting features
Microtec.PublicApi.AppsPortalHTTP clients for AppsPortal APICross-service calls

Code Style Rules

These are enforced by the team's .editorconfig and code review.

  1. File-scoped namespaces: namespace Microtec.AppsPortal.Application.Invoice.Commands.Add;
  2. Primary constructors for DI: public class MyHandler(IUnitOfWork uow) { ... }
  3. Early returns: Don't nest — return early on validation failures
  4. Decompose large methods: Functions > 20 lines should be extracted
  5. nameof() for route strings: [Route(nameof(GetById))] not [Route("GetById")]
  6. ProducesResponseType on all actions:
    csharp
    [ProducesResponseType<int>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> AddInvoice(...) { ... }
  7. Rich domain entities with private setters:
    csharp
    public class Invoice : BaseEntity
    {
        public string Number { get; private set; } = default!;
        public static Invoice Create(string number) => new() { Number = number };
    }

Full code style reference: Platforms/Solution Items/CodeStyle.md
Full naming conventions: Platforms/Solution Items/NamingConventions.md


Testing Strategy

bash
# Run all backend tests
dotnet test Microtec.Platforms.sln

# Run with coverage
dotnet test Microtec.Platforms.sln \
  --collect:"XPlat Code Coverage" \
  --results-directory TestResults/

# Generate coverage report
reportgenerator -reports:"TestResults/**/*.xml" -targetdir:"coverage-report"

Target: 80% code coverage minimum (enforced in CI).


Useful IDE Shortcuts (Rider)

ActionShortcut
Find usagesAlt+F7
Navigate to implementationCtrl+Alt+B
Run tests in current fileCtrl+Shift+T
Apply code cleanupCtrl+Alt+F
Find in filesCtrl+Shift+F
Navigate to fileCtrl+Shift+N

Internal Documentation — Microtec Platform Team