mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 02:57:36 +00:00
refining of API desing doc
This commit is contained in:
@@ -95,26 +95,49 @@ These methods return tuples with an optional error `ActionResult`, enabling earl
|
|||||||
|
|
||||||
### Services (`src/Services/`)
|
### Services (`src/Services/`)
|
||||||
|
|
||||||
Services encapsulate business logic that spans multiple repositories or involves non-trivial orchestration. Register them via DI as scoped services.
|
Services encapsulate business logic that spans multiple repositories or involves non-trivial orchestration. Services are either static classes (for stateless logic) or singletons instantiated directly where needed.
|
||||||
|
|
||||||
**When to use a service vs. calling a repository directly from a controller:**
|
**When to use a service vs. calling a repository directly from a controller:**
|
||||||
- **Direct repository call:** Simple CRUD with no cross-cutting logic
|
- **Direct repository call:** Simple CRUD with no cross-cutting logic
|
||||||
- **Service:** Multi-step operations, external API calls, or any logic spanning multiple repositories
|
- **Service:** Multi-step operations, external API calls, or any logic spanning multiple repositories
|
||||||
|
|
||||||
**Module registration pattern** for grouping related services:
|
**Static service pattern** (preferred for stateless logic):
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public static class MyModuleExtensions
|
public static class MyService
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddMyModule(this IServiceCollection services)
|
public static async Task<Result> DoSomethingAsync(Guid entityId)
|
||||||
{
|
{
|
||||||
services.AddScoped<IMyService, MyService>();
|
var repo = new ExampleRepository();
|
||||||
services.AddScoped<MyRepository>();
|
// orchestration logic
|
||||||
return services;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Singleton instance pattern** (when the service needs initialization or holds config):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyService
|
||||||
|
{
|
||||||
|
public static MyService Instance { get; private set; } = null!;
|
||||||
|
|
||||||
|
public static void Initialize(string apiKey, string baseUrl)
|
||||||
|
{
|
||||||
|
Instance = new MyService { _apiKey = apiKey, _baseUrl = baseUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _apiKey;
|
||||||
|
private string _baseUrl;
|
||||||
|
|
||||||
|
public async Task<Result> DoSomethingAsync() { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// In Program.cs
|
||||||
|
MyService.Initialize(
|
||||||
|
builder.Configuration["MyService:ApiKey"]!,
|
||||||
|
builder.Configuration["MyService:BaseUrl"]!);
|
||||||
|
```
|
||||||
|
|
||||||
### Repositories (`DataAccess/Repositories/`)
|
### 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.
|
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.
|
||||||
@@ -252,32 +275,30 @@ Domain-specific response classes map from database objects, excluding internal f
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Options Pattern
|
### Configuration Helper
|
||||||
|
|
||||||
External service configuration uses the ASP.NET Core options pattern. Each integration gets its own options class.
|
A static helper class provides access to `IConfiguration` from anywhere in the application, without dependency injection.
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Configuration/MyServiceOptions.cs
|
// Configuration/ConfigHelper.cs
|
||||||
public class MyServiceOptions
|
public static class ConfigHelper
|
||||||
{
|
{
|
||||||
public const string SectionName = "MyService";
|
public static IConfiguration Configuration { get; set; } = null!;
|
||||||
public required string ApiKey { get; set; }
|
|
||||||
public required string BaseUrl { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// appsettings.json
|
// Program.cs (before any services are used)
|
||||||
{
|
ConfigHelper.Configuration = builder.Configuration;
|
||||||
"MyService": {
|
|
||||||
"ApiKey": "...",
|
|
||||||
"BaseUrl": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Program.cs
|
|
||||||
services.Configure<MyServiceOptions>(
|
|
||||||
builder.Configuration.GetSection(MyServiceOptions.SectionName));
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Configuration values are read directly where needed:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var apiKey = ConfigHelper.Configuration["MyService:ApiKey"];
|
||||||
|
var baseUrl = ConfigHelper.Configuration["MyService:BaseUrl"];
|
||||||
|
```
|
||||||
|
|
||||||
|
For services that need config at initialization, pass values explicitly during setup in `Program.cs` rather than reading config inside the service.
|
||||||
|
|
||||||
### DatabaseManager
|
### DatabaseManager
|
||||||
|
|
||||||
A static class that provides the connection string to all repositories. Configures Dapper's snake_case-to-PascalCase column mapping.
|
A static class that provides the connection string to all repositories. Configures Dapper's snake_case-to-PascalCase column mapping.
|
||||||
@@ -355,12 +376,12 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
|
|
||||||
### Token Service
|
### 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).
|
A static helper class that generates JWTs with custom claims. Reads JWT config via `ConfigHelper`. Add whatever domain-specific claims your application requires (e.g., user ID, tenant/org ID, email).
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class TokenService
|
public static class TokenService
|
||||||
{
|
{
|
||||||
public string GenerateToken(Guid userId, string email, List<string> roles)
|
public static string GenerateToken(Guid userId, string email, List<string> roles)
|
||||||
{
|
{
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
@@ -370,9 +391,10 @@ public class TokenService
|
|||||||
// Add domain-specific claims as needed
|
// Add domain-specific claims as needed
|
||||||
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
|
||||||
|
|
||||||
var key = new SymmetricSecurityKey(Convert.FromBase64String(_config["Jwt:Key"]!));
|
var key = new SymmetricSecurityKey(
|
||||||
|
Convert.FromBase64String(ConfigHelper.Configuration["Jwt:Key"]!));
|
||||||
var token = new JwtSecurityToken(
|
var token = new JwtSecurityToken(
|
||||||
issuer: _config["Jwt:Issuer"],
|
issuer: ConfigHelper.Configuration["Jwt:Issuer"],
|
||||||
expires: DateTime.UtcNow.AddMinutes(15),
|
expires: DateTime.UtcNow.AddMinutes(15),
|
||||||
claims: claims,
|
claims: claims,
|
||||||
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
|
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
|
||||||
@@ -434,29 +456,24 @@ Captures request/response details to the database for observability:
|
|||||||
```csharp
|
```csharp
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// --- Static configuration (must come first) ---
|
||||||
|
ConfigHelper.Configuration = builder.Configuration;
|
||||||
|
|
||||||
// --- Kestrel configuration ---
|
// --- Kestrel configuration ---
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
{
|
{
|
||||||
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
|
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50 MB
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Configuration binding ---
|
|
||||||
builder.Services.Configure<MyServiceOptions>(
|
|
||||||
builder.Configuration.GetSection(MyServiceOptions.SectionName));
|
|
||||||
|
|
||||||
// --- Authentication ---
|
// --- Authentication ---
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options => { /* see JWT Configuration above */ });
|
.AddJwtBearer(options => { /* see JWT Configuration above */ });
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
// --- Service registration ---
|
// --- Controllers ---
|
||||||
builder.Services.AddScoped<TokenService>();
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
// Module registrations
|
// --- Background services (if needed) ---
|
||||||
builder.Services.AddMyModule();
|
|
||||||
|
|
||||||
// Background services (if needed)
|
|
||||||
// builder.Services.AddHostedService<MyBackgroundWorker>();
|
// builder.Services.AddHostedService<MyBackgroundWorker>();
|
||||||
|
|
||||||
// --- CORS ---
|
// --- CORS ---
|
||||||
@@ -477,6 +494,11 @@ builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|||||||
options.KnownProxies.Clear();
|
options.KnownProxies.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Initialize singleton services ---
|
||||||
|
MyService.Initialize(
|
||||||
|
builder.Configuration["MyService:ApiKey"]!,
|
||||||
|
builder.Configuration["MyService:BaseUrl"]!);
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
@@ -493,18 +515,17 @@ app.Run();
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dependency Registration Summary
|
## Instantiation Patterns
|
||||||
|
|
||||||
| Registration | Lifetime | Purpose |
|
| Component | Pattern | Example |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `TokenService` | Scoped | JWT generation |
|
| Repositories | `new` in controllers | `private readonly ExampleRepository _repo = new();` |
|
||||||
| `IMyService` / `MyService` | Scoped | Business logic |
|
| Stateless services | Static class | `TokenService.GenerateToken(...)` |
|
||||||
| Options classes | Singleton (via `Configure<T>`) | External service config |
|
| Stateful services | Singleton with `Initialize` | `MyService.Instance.DoSomething()` |
|
||||||
| External API wrappers | Singleton | Stateless third-party service clients |
|
| Configuration | Static helper | `ConfigHelper.Configuration["Key"]` |
|
||||||
| Background workers | Hosted Service | Async background processing |
|
| Background workers | Hosted Service (only DI used) | `builder.Services.AddHostedService<MyWorker>()` |
|
||||||
| 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.
|
The only use of the DI container is for framework-level concerns that require it: authentication, authorization, CORS, hosted background services, and forwarded headers. All application-level code uses static classes or direct instantiation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -526,8 +547,8 @@ Repositories are instantiated directly in controllers (`new ExampleRepository()`
|
|||||||
3. **Snake_case DB columns** mapped automatically to PascalCase C# properties
|
3. **Snake_case DB columns** mapped automatically to PascalCase C# properties
|
||||||
4. **Standard response envelope** (`ResponseResult<T>`) on all endpoints
|
4. **Standard response envelope** (`ResponseResult<T>`) on all endpoints
|
||||||
5. **Claims-based authorization** with role constants and BaseController helpers
|
5. **Claims-based authorization** with role constants and BaseController helpers
|
||||||
6. **Options pattern** for all external configuration
|
6. **Static configuration helper** for accessing `appsettings.json` anywhere
|
||||||
7. **Module extension methods** for grouping related service registrations
|
7. **No dependency injection** for application code — static classes and direct instantiation only
|
||||||
8. **Async/await throughout** — no synchronous database calls
|
8. **Async/await throughout** — no synchronous database calls
|
||||||
9. **Soft deletes** via `Deleted` boolean flag where needed
|
9. **Soft deletes** via `Deleted` boolean flag where needed
|
||||||
10. **Audit timestamps** (`CreatedAt`, `UpdatedAt`) on all entities
|
10. **Audit timestamps** (`CreatedAt`, `UpdatedAt`) on all entities
|
||||||
|
|||||||
Reference in New Issue
Block a user