Skip to content

Backend Conventions

These rules apply to every module in MedRegia.API. They are enforced in code reviews.

Layer Rules

Domain Layer — POCOs Only

csharp
// ✅ Correct
public class Vendor
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public VendorType VendorType { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}

// ❌ Wrong — no [Required], no [Key], no navigation properties, no logic
public class Vendor
{
    [Key]
    [Required]
    public int Id { get; set; }
    public List<Invoice> Invoices { get; set; } = []; // no EF navigation
    public string GetDisplayName() => Name; // no methods
}

Data Layer — SQL Only

csharp
// ✅ Correct — inject IDbConnectionFactory, pure SQL
public class VendorRepository : IVendorRepository
{
    private readonly IDbConnectionFactory _dbFactory;
    public VendorRepository(IDbConnectionFactory dbFactory) => _dbFactory = dbFactory;

    public async Task<Vendor?> GetByIdAsync(int id)
    {
        using var conn = _dbFactory.CreateConnection();
        return await conn.QuerySingleOrDefaultAsync<Vendor>(
            "SELECT * FROM vendors WHERE id = @Id AND is_active = 1",
            new { Id = id });
    }
}

// ❌ Wrong — no new MySqlConnection(), no business logic in repositories
public class VendorRepository
{
    public async Task<Vendor?> GetByIdAsync(int id)
    {
        using var conn = new MySqlConnection("...");  // NEVER
        var vendor = await conn.QuerySingleOrDefaultAsync<Vendor>(...);
        if (vendor == null) throw new Exception("Not found"); // NEVER throw here
        return vendor;
    }
}

Business Layer — All Logic Here

csharp
// ✅ Correct
public class VendorService : IVendorService
{
    public async Task<Vendor> CreateVendorAsync(Vendor vendor)
    {
        vendor.CreatedAt = DateTime.UtcNow;  // server sets timestamps
        vendor.UpdatedAt = DateTime.UtcNow;
        vendor.IsActive = true;
        return await _repository.CreateAsync(vendor);
    }

    public async Task<bool> DeleteVendorAsync(int id)
    {
        var existing = await _repository.GetByIdAsync(id);
        if (existing == null) return false;  // return null/false, never throw
        return await _repository.DeleteAsync(id);
    }
}

Controller Layer — HTTP Interface Only

csharp
// ✅ Correct
[HttpGet("{id}")]
[RequirePermission("Vendor", "Read")]
[ProducesResponseType<VendorDto>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<VendorDto>> GetVendor(int id)
{
    var vendor = await _vendorService.GetVendorByIdAsync(id);
    if (vendor == null) return NotFound();
    return Ok(_mapper.Map<VendorDto>(vendor));
}

Pagination Convention

All list endpoints return PagedResult<T>:

json
{
  "items": [...],
  "totalCount": 42,
  "page": 1,
  "pageSize": 10
}

SQL pattern:

sql
SELECT * FROM vendors WHERE is_active = 1 LIMIT @PageSize OFFSET @Offset
-- Offset = (page - 1) * pageSize

Soft Delete Convention

Records are never hard-deleted. Use is_active = 0:

sql
UPDATE vendors SET is_active = 0, updated_at = NOW() WHERE id = @Id

Optionally capture a reason:

sql
UPDATE vendors SET is_active = 0, delete_reason = @Reason, updated_at = NOW() WHERE id = @Id

Dependency Injection

All registrations go in Infrastructure/DependencyInjection.cs:

csharp
public static IServiceCollection AddModules(this IServiceCollection services)
{
    // Identity
    services.AddScoped<IUserRepository, UserRepository>();
    services.AddScoped<IUserService, UserService>();
    // ... every repo and service must be registered
    return services;
}

Missing Registration

A missing DI registration causes a runtime InvalidOperationException on the first request to that endpoint. Always add registrations immediately when creating a new repository or service.

Error Responses

The framework automatically generates ProblemDetails for all non-2xx responses:

json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "Not Found",
  "status": 404,
  "traceId": "00-abc123..."
}

Never manually build error objects — use NotFound(), BadRequest(), Forbid() etc.

Enum Type Handlers

Every C# enum that maps to a VARCHAR column needs a Dapper type handler registered in Infrastructure/Persistence/EnumTypeHandlers.cs and registered in Program.cs before any query runs:

csharp
// EnumTypeHandlers.cs
public class VendorTypeHandler : SqlMapper.TypeHandler<VendorType>
{
    public override void SetValue(IDbDataParameter p, VendorType v) => p.Value = v.ToString();
    public override VendorType Parse(object v) => Enum.Parse<VendorType>(v.ToString()!, true);
}

// Program.cs — must be at the very top, before builder creation
SqlMapper.AddTypeHandler(new VendorTypeHandler());

No any / No null Throws

  • Never use object as a catch-all return type
  • Never throw exceptions for expected not-found scenarios — return null from services
  • Never inject repositories into controllers — always use service interfaces

MedRegia — Pharmaceutical Vendor Management Platform