mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 05:17:41 +00:00
latest
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: winstudent-mysql
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "3309:3306"
|
||||||
|
volumes:
|
||||||
|
- win_mysql_data:/var/lib/mysql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
win_mysql_data:
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
# .NET API Project Design Document
|
||||||
|
|
||||||
|
A reference architecture for building ASP.NET Core Web APIs with Dapper, JWT authentication, and a co-located data access layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
All code lives in a single project. The data access layer is organized as a namespace within the same assembly rather than a separate project.
|
||||||
|
|
||||||
|
```
|
||||||
|
MyApi/
|
||||||
|
├── MyApi.sln
|
||||||
|
├── MyApi.csproj
|
||||||
|
├── Program.cs
|
||||||
|
├── appsettings.json
|
||||||
|
├── appsettings.Development.json
|
||||||
|
├── Dockerfile
|
||||||
|
├── Configuration/
|
||||||
|
│ └── (Options classes for external services)
|
||||||
|
├── DataAccess/
|
||||||
|
│ ├── DatabaseManager.cs
|
||||||
|
│ ├── Models/
|
||||||
|
│ │ ├── DatabaseObjects/
|
||||||
|
│ │ └── DataTransferObjects/
|
||||||
|
│ └── Repositories/
|
||||||
|
└── src/
|
||||||
|
├── BaseClasses/
|
||||||
|
│ └── BaseController.cs
|
||||||
|
├── Controllers/
|
||||||
|
├── Middleware/
|
||||||
|
├── Models/
|
||||||
|
│ └── ResponseTypes/
|
||||||
|
└── Services/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer Responsibilities
|
||||||
|
|
||||||
|
### Controllers (`src/Controllers/`)
|
||||||
|
|
||||||
|
Controllers handle HTTP concerns only: request validation, claims extraction, and response shaping. They delegate all business logic to services and all data access to repositories.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ExampleController : BaseController
|
||||||
|
{
|
||||||
|
private readonly ExampleRepository _exampleRepository = new();
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<ResponseResult<ExampleResponse>>> GetById(Guid id)
|
||||||
|
{
|
||||||
|
var (userId, error) = GetUserIdFromClaims();
|
||||||
|
if (error != null) return error;
|
||||||
|
|
||||||
|
var entity = await _exampleRepository.GetByIdAsync(id);
|
||||||
|
if (entity == null) return NotFound();
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<ExampleResponse>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = MapToResponse(entity)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conventions:**
|
||||||
|
- Use `[ApiController]` and `[Route("api/[controller]")]` on every controller
|
||||||
|
- Inherit from `BaseController` for claims extraction helpers
|
||||||
|
- Return `ActionResult<ResponseResult<T>>` for consistent response envelopes
|
||||||
|
- Apply `[Authorize]` or `[Authorize(Roles = "...")]` per-endpoint
|
||||||
|
- Use `[ProducesResponseType]` attributes for OpenAPI documentation
|
||||||
|
|
||||||
|
### Base Controller (`src/BaseClasses/BaseController.cs`)
|
||||||
|
|
||||||
|
Provides protected helper methods that extract and validate JWT claims, reducing boilerplate across controllers.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class BaseController : ControllerBase
|
||||||
|
{
|
||||||
|
protected (Guid userId, ActionResult? error) GetUserIdFromClaims() { ... }
|
||||||
|
protected (string email, List<string> roles, ActionResult? error) GetUserDetailsFromClaims() { ... }
|
||||||
|
protected bool HasRole(string role) { ... }
|
||||||
|
protected bool HasAnyRole(params string[] roles) { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add additional claim extraction helpers as needed for your domain (e.g., tenant ID, organization ID).
|
||||||
|
|
||||||
|
These methods return tuples with an optional error `ActionResult`, enabling early-return patterns in controller actions.
|
||||||
|
|
||||||
|
### Services (`src/Services/`)
|
||||||
|
|
||||||
|
Services encapsulate business logic that spans multiple repositories or involves non-trivial orchestration. Register them via DI as scoped services.
|
||||||
|
|
||||||
|
**When to use a service vs. calling a repository directly from a controller:**
|
||||||
|
- **Direct repository call:** Simple CRUD with no cross-cutting logic
|
||||||
|
- **Service:** Multi-step operations, external API calls, or any logic spanning multiple repositories
|
||||||
|
|
||||||
|
**Module registration pattern** for grouping related services:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class MyModuleExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddMyModule(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IMyService, MyService>();
|
||||||
|
services.AddScoped<MyRepository>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repositories (`DataAccess/Repositories/`)
|
||||||
|
|
||||||
|
Each repository maps to a single domain entity and serves as a thin C# wrapper around stored procedures. Repositories create their own database connections per call and are responsible for calling stored procedures and assembling the results into typed objects.
|
||||||
|
|
||||||
|
**All query logic lives in stored procedures in the database — repositories contain no inline SQL.**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ExampleRepository
|
||||||
|
{
|
||||||
|
private IDbConnection Connection =>
|
||||||
|
new MySqlConnection(DatabaseManager.ConnectionString);
|
||||||
|
|
||||||
|
public async Task<dbExample?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QuerySingleOrDefaultAsync<dbExample>(
|
||||||
|
"sp_Example_GetById",
|
||||||
|
new { Id = id },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<dbExample>> GetByOwnerAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
return await db.QueryAsync<dbExample>(
|
||||||
|
"sp_Example_GetByOwner",
|
||||||
|
new { OwnerId = ownerId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> InsertAsync(dbExample entity)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rows = await db.ExecuteAsync(
|
||||||
|
"sp_Example_Insert",
|
||||||
|
new { entity.Id, entity.Name, entity.CreatedAt },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For stored procedures that return multiple result sets,
|
||||||
|
/// use QueryMultipleAsync to assemble a composite object.
|
||||||
|
public async Task<ExampleWithDetails?> GetWithDetailsAsync(Guid id)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
using var multi = await db.QueryMultipleAsync(
|
||||||
|
"sp_Example_GetWithDetails",
|
||||||
|
new { Id = id },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
var example = await multi.ReadSingleOrDefaultAsync<dbExample>();
|
||||||
|
if (example == null) return null;
|
||||||
|
|
||||||
|
var tags = (await multi.ReadAsync<dbTag>()).ToList();
|
||||||
|
var history = (await multi.ReadAsync<dbHistoryEntry>()).ToList();
|
||||||
|
|
||||||
|
return new ExampleWithDetails
|
||||||
|
{
|
||||||
|
Example = example,
|
||||||
|
Tags = tags,
|
||||||
|
History = history
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conventions:**
|
||||||
|
- One repository per domain entity
|
||||||
|
- All methods are `async Task<T>`
|
||||||
|
- Use `using var db = Connection;` to ensure connection disposal
|
||||||
|
- **All data access goes through stored procedures** — no inline SQL in repositories
|
||||||
|
- Always pass `commandType: CommandType.StoredProcedure` to Dapper calls
|
||||||
|
- Use `QueryMultipleAsync` when a stored procedure returns multiple result sets, and assemble the results into composite objects
|
||||||
|
- Return `db`-prefixed model types from database operations
|
||||||
|
- Stored procedure naming convention: `sp_{Entity}_{Action}` (e.g., `sp_Example_GetById`, `sp_User_Insert`)
|
||||||
|
|
||||||
|
### Database Objects (`DataAccess/Models/DatabaseObjects/`)
|
||||||
|
|
||||||
|
Plain C# classes that map directly to database tables. Use the `db` prefix to distinguish them from response DTOs.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class dbExample
|
||||||
|
{
|
||||||
|
public required Guid Id { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool Deleted { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conventions:**
|
||||||
|
- `db` prefix on all database object class names
|
||||||
|
- Use `required` for non-nullable columns
|
||||||
|
- Use nullable types (`string?`, `Guid?`) for optional columns
|
||||||
|
- Include `CreatedAt` and `UpdatedAt` timestamps on all entities
|
||||||
|
- Use `bool Deleted` for soft-delete support where needed
|
||||||
|
|
||||||
|
### Data Transfer Objects (`DataAccess/Models/DataTransferObjects/`)
|
||||||
|
|
||||||
|
DTOs define the shape of data entering repositories from controllers. They are distinct from both database objects and response types.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class CreateExampleDTO
|
||||||
|
{
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateExampleDTO
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Types (`src/Models/ResponseTypes/`)
|
||||||
|
|
||||||
|
Response types define the shape of data returned to API consumers. Use a standard envelope.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ResponseResult<T>
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public T? Data { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Domain-specific response classes map from database objects, excluding internal fields and reshaping data for client consumption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Options Pattern
|
||||||
|
|
||||||
|
External service configuration uses the ASP.NET Core options pattern. Each integration gets its own options class.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Configuration/MyServiceOptions.cs
|
||||||
|
public class MyServiceOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "MyService";
|
||||||
|
public required string ApiKey { get; set; }
|
||||||
|
public required string BaseUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// appsettings.json
|
||||||
|
{
|
||||||
|
"MyService": {
|
||||||
|
"ApiKey": "...",
|
||||||
|
"BaseUrl": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program.cs
|
||||||
|
services.Configure<MyServiceOptions>(
|
||||||
|
builder.Configuration.GetSection(MyServiceOptions.SectionName));
|
||||||
|
```
|
||||||
|
|
||||||
|
### DatabaseManager
|
||||||
|
|
||||||
|
A static class that provides the connection string to all repositories. Configures Dapper's snake_case-to-PascalCase column mapping.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class DatabaseManager
|
||||||
|
{
|
||||||
|
private static IConfiguration? _configuration;
|
||||||
|
|
||||||
|
private static IConfiguration Configuration
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_configuration == null)
|
||||||
|
{
|
||||||
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
|
_configuration = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
return _configuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConnectionString =>
|
||||||
|
Configuration.GetConnectionString("DefaultConnection")
|
||||||
|
?? throw new MissingFieldException("DefaultConnection not configured");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### appsettings.json Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Jwt": {
|
||||||
|
"Key": "<base64-encoded-signing-key>",
|
||||||
|
"Issuer": "MyApi"
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"DefaultConnection": "Server=...;Database=...;Uid=...;Pwd=...;Pooling=true;Max Pool Size=200;Min Pool Size=5;"
|
||||||
|
},
|
||||||
|
"MyService": { ... },
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication & Authorization
|
||||||
|
|
||||||
|
### JWT Configuration
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(
|
||||||
|
Convert.FromBase64String(builder.Configuration["Jwt:Key"]!)),
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Service
|
||||||
|
|
||||||
|
A scoped service that generates JWTs with custom claims. Add whatever domain-specific claims your application requires (e.g., user ID, tenant/org ID, email).
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class TokenService
|
||||||
|
{
|
||||||
|
public string GenerateToken(Guid userId, string email, List<string> roles)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new("user_id", userId.ToString()),
|
||||||
|
new(ClaimTypes.Email, email)
|
||||||
|
};
|
||||||
|
// Add domain-specific claims as needed
|
||||||
|
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||||
|
|
||||||
|
var key = new SymmetricSecurityKey(Convert.FromBase64String(_config["Jwt:Key"]!));
|
||||||
|
var token = new JwtSecurityToken(
|
||||||
|
issuer: _config["Jwt:Issuer"],
|
||||||
|
expires: DateTime.UtcNow.AddMinutes(15),
|
||||||
|
claims: claims,
|
||||||
|
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
|
||||||
|
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role-Based Authorization
|
||||||
|
|
||||||
|
Define roles as string constants in a static class and apply them via attributes.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class UserRoles
|
||||||
|
{
|
||||||
|
public const string Admin = "Admin";
|
||||||
|
public const string User = "User";
|
||||||
|
// Add application-specific roles as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage on endpoints
|
||||||
|
[Authorize(Roles = UserRoles.Admin)]
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<ActionResult> Delete(Guid id) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Middleware Pipeline
|
||||||
|
|
||||||
|
Configure middleware in `Program.cs` in the following order:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseMiddleware<GlobalExceptionHandler>(); // 1. Catch unhandled exceptions
|
||||||
|
app.UseMiddleware<RequestLoggingMiddleware>(); // 2. Log all requests/responses
|
||||||
|
app.UseCors(); // 3. CORS policy
|
||||||
|
app.UseHttpsRedirection(); // 4. HTTPS enforcement
|
||||||
|
app.UseAuthentication(); // 5. JWT validation
|
||||||
|
app.UseAuthorization(); // 6. Role/policy checks
|
||||||
|
app.MapControllers(); // 7. Route to controllers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request Logging Middleware
|
||||||
|
|
||||||
|
Captures request/response details to the database for observability:
|
||||||
|
- HTTP method, path, query string, status code
|
||||||
|
- Request and response bodies (excluding multipart/form-data)
|
||||||
|
- User ID extracted from JWT claims
|
||||||
|
- Processing time in milliseconds
|
||||||
|
- Request trace ID for correlation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Program.cs Template
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// --- Kestrel configuration ---
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Configuration binding ---
|
||||||
|
builder.Services.Configure<MyServiceOptions>(
|
||||||
|
builder.Configuration.GetSection(MyServiceOptions.SectionName));
|
||||||
|
|
||||||
|
// --- Authentication ---
|
||||||
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options => { /* see JWT Configuration above */ });
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// --- Service registration ---
|
||||||
|
builder.Services.AddScoped<TokenService>();
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// Module registrations
|
||||||
|
builder.Services.AddMyModule();
|
||||||
|
|
||||||
|
// Background services (if needed)
|
||||||
|
// builder.Services.AddHostedService<MyBackgroundWorker>();
|
||||||
|
|
||||||
|
// --- CORS ---
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddDefaultPolicy(policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Forwarded headers (for reverse proxy) ---
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders =
|
||||||
|
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
options.KnownNetworks.Clear();
|
||||||
|
options.KnownProxies.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
app.UseMiddleware<GlobalExceptionHandler>();
|
||||||
|
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||||
|
app.UseCors();
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Registration Summary
|
||||||
|
|
||||||
|
| Registration | Lifetime | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `TokenService` | Scoped | JWT generation |
|
||||||
|
| `IMyService` / `MyService` | Scoped | Business logic |
|
||||||
|
| Options classes | Singleton (via `Configure<T>`) | External service config |
|
||||||
|
| External API wrappers | Singleton | Stateless third-party service clients |
|
||||||
|
| Background workers | Hosted Service | Async background processing |
|
||||||
|
| Repositories | Instantiated directly | Data access (no DI needed) |
|
||||||
|
|
||||||
|
Repositories are instantiated directly in controllers (`new ExampleRepository()`) rather than registered in DI. This is appropriate because they are stateless and create their own connections per call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Technology Choices
|
||||||
|
|
||||||
|
| Concern | Technology |
|
||||||
|
|---|---|
|
||||||
|
| Framework | ASP.NET Core 9.0 |
|
||||||
|
| ORM | Dapper (micro-ORM, stored procedure calls only) |
|
||||||
|
| Database | MySQL via MySql.Data |
|
||||||
|
| Authentication | JWT Bearer tokens |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Patterns & Conventions Summary
|
||||||
|
|
||||||
|
1. **Repository pattern** with one repository per domain entity
|
||||||
|
2. **Stored procedures exclusively** — all query logic lives in the database, repositories are thin wrappers
|
||||||
|
3. **Snake_case DB columns** mapped automatically to PascalCase C# properties
|
||||||
|
4. **Standard response envelope** (`ResponseResult<T>`) on all endpoints
|
||||||
|
5. **Claims-based authorization** with role constants and BaseController helpers
|
||||||
|
6. **Options pattern** for all external configuration
|
||||||
|
7. **Module extension methods** for grouping related service registrations
|
||||||
|
8. **Async/await throughout** — no synchronous database calls
|
||||||
|
9. **Soft deletes** via `Deleted` boolean flag where needed
|
||||||
|
10. **Audit timestamps** (`CreatedAt`, `UpdatedAt`) on all entities
|
||||||
Reference in New Issue
Block a user