Appearance
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) * pageSizeSoft Delete Convention
Records are never hard-deleted. Use is_active = 0:
sql
UPDATE vendors SET is_active = 0, updated_at = NOW() WHERE id = @IdOptionally capture a reason:
sql
UPDATE vendors SET is_active = 0, delete_reason = @Reason, updated_at = NOW() WHERE id = @IdDependency 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
objectas a catch-all return type - Never throw exceptions for expected not-found scenarios — return
nullfrom services - Never inject repositories into controllers — always use service interfaces