Draft
This web application architecture guidance is still in draft and is subject to change.
ASP.NET Quick Reference
This page provides quick-reference tables and code snippets for common ASP.NET Core patterns used in RMC web applications.
Common Attributes
| Attribute | Purpose | Example |
|---|---|---|
| [ApiController] | Marks class as API controller | [ApiController] class MyController |
| [Route("path")] | Sets base URL path | [Route("api/analyses")] |
| [HttpGet], [HttpPost], etc. | HTTP method for action | [HttpPost("calculate")] |
| [Authorize] | Requires authentication | [Authorize] class or method |
| [AllowAnonymous] | Allows unauthenticated access | [AllowAnonymous] public Login() |
| [FromBody] | Bind from JSON body | Create([FromBody] Dto dto) |
| [FromQuery] | Bind from query string | List([FromQuery] int page) |
| [FromRoute] | Bind from URL path | [HttpGet("{id}")] Get(Guid id) |
| [Required] | Validation: field required | [Required] public string Name |
| [MaxLength(n)] | Validation: max string length | [MaxLength(255)] public string |
Response Methods
| HTTP Status | ASP.NET Method | When to Use |
|---|---|---|
| 200 OK | return Ok(data) | Successful GET or calculation |
| 201 Created | return Created(url, data) | Successful POST (item created) |
| 204 No Content | return NoContent() | Successful DELETE |
| 400 Bad Request | return BadRequest(error) | Validation failed |
| 401 Unauthorized | return Unauthorized() | No/invalid authentication token |
| 403 Forbidden | return Forbid() | User lacks permission |
| 404 Not Found | return NotFound() | Resource does not exist |
| 409 Conflict | return Conflict(error) | Resource state conflict |
| 500 Server Error | return StatusCode(500, error) | Unhandled server error |
Database Operations (Entity Framework)
| Operation | Code | Notes |
|---|---|---|
| Get by ID | await _db.Analyses.FindAsync(id) | Returns null if not found |
| Get all | await _db.Analyses.ToListAsync() | Returns empty list if none |
| Filter by field | _db.Analyses.Where(a => a.UserId == uid) | Returns IQueryable (not executed yet) |
| Filter (complex) | _db.Analyses.Where(a => a.X > 5) | Chain with .ToListAsync() |
| First match | _db.Analyses.FirstOrDefaultAsync(a => ...) | Returns null if not found |
| Count | await _db.Analyses.CountAsync() | Executes query |
| Create | _db.Analyses.Add(analysis) | Entity tracked, not yet saved |
| Update | analysis.Name = "New" | Entity must be tracked |
| Delete | _db.Analyses.Remove(analysis) | Entity tracked for deletion |
| Save changes | await _db.SaveChangesAsync() | Commits all pending changes |
Controller Template
Basic CRUD Controller
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AnalysesController : ControllerBase
{
private readonly AppDbContext _db;
public AnalysesController(AppDbContext db) => _db = db;
// GET /api/analyses
[HttpGet]
public async Task<IActionResult> GetAll()
{
var userId = GetCurrentUserId();
var items = await _db.Analyses
.Where(a => a.UserId == userId)
.ToListAsync();
return Ok(items);
}
// GET /api/analyses/{id}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(Guid id)
{
var item = await _db.Analyses.FindAsync(id);
if (item == null) return NotFound();
if (item.UserId != GetCurrentUserId()) return Forbid();
return Ok(item);
}
// POST /api/analyses
[HttpPost]
public async Task<IActionResult> Create([FromBody] AnalysisDto dto)
{
var analysis = new Analysis
{
Name = dto.Name,
UserId = GetCurrentUserId()
};
_db.Analyses.Add(analysis);
await _db.SaveChangesAsync();
return Created($"/api/analyses/{analysis.Id}", analysis);
}
// PUT /api/analyses/{id}
[HttpPut("{id}")]
public async Task<IActionResult> Update(Guid id, [FromBody] AnalysisDto dto)
{
var analysis = await _db.Analyses.FindAsync(id);
if (analysis == null) return NotFound();
if (analysis.UserId != GetCurrentUserId()) return Forbid();
analysis.Name = dto.Name;
analysis.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
return Ok(analysis);
}
// DELETE /api/analyses/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid id)
{
var analysis = await _db.Analyses.FindAsync(id);
if (analysis == null) return NotFound();
if (analysis.UserId != GetCurrentUserId()) return Forbid();
_db.Analyses.Remove(analysis);
await _db.SaveChangesAsync();
return NoContent();
}
private string GetCurrentUserId() =>
User.FindFirstValue(ClaimTypes.NameIdentifier)!;
}
Calculation Endpoint
[HttpPost("calculate/{analysisId}")]
public async Task<IActionResult> Calculate(Guid analysisId)
{
// 1. Get current user
var userId = GetCurrentUserId();
// 2. Load analysis
var analysis = await _db.Analyses.FindAsync(analysisId);
if (analysis == null) return NotFound();
// 3. Check authorization
if (analysis.UserId != userId) return Forbid();
// 4. Run calculation (via HTTP client or service)
var result = await _calculationService.RunAsync(analysisId, userId);
// 5. Save results
analysis.ResultData = JsonSerializer.SerializeToDocument(result.Data, JsonDefaults.CamelCase);
analysis.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
// 6. Return messaging contract response
return Ok(new ApiResponse<object>
{
Status = "success",
Data = result.Data,
Warnings = result.Warnings,
CanRun = true
});
}
Entity Template
Domain entities are POCOs (Plain Old CLR Objects) with no data annotations. Database mapping is configured separately via Fluent API (see Backend Architecture — Entity Configuration).
// Domain/Entities/Analysis.cs
public class Analysis
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// JSONB columns for flexible data
public JsonDocument? Parameters { get; set; }
public JsonDocument? ResultData { get; set; }
// Navigation property
public User User { get; set; } = null!;
}
// Infrastructure/Data/Configurations/AnalysisConfiguration.cs
public class AnalysisConfiguration : IEntityTypeConfiguration<Analysis>
{
public void Configure(EntityTypeBuilder<Analysis> builder)
{
builder.ToTable("analyses");
builder.HasKey(a => a.Id);
builder.Property(a => a.Id).HasDefaultValueSql("gen_random_uuid()");
builder.Property(a => a.Name).IsRequired().HasMaxLength(255);
builder.Property(a => a.UserId).IsRequired();
builder.Property(a => a.Parameters).HasColumnType("jsonb");
builder.Property(a => a.ResultData).HasColumnType("jsonb");
builder.Property(a => a.CreatedAt).HasDefaultValueSql("NOW()");
builder.HasIndex(a => a.UserId);
builder.HasOne<User>().WithMany().HasForeignKey(a => a.UserId);
}
}
Logging Patterns
public class AnalysisController : ControllerBase
{
private readonly ILogger<AnalysisController> _logger;
public AnalysisController(ILogger<AnalysisController> logger)
{
_logger = logger;
}
[HttpPost("calculate/{id}")]
public async Task<IActionResult> Calculate(Guid id)
{
// Use structured logging with named parameters
_logger.LogInformation(
"[API] POST /api/analysis/calculate/{AnalysisId}",
id);
try
{
_logger.LogDebug(
"[CALC] Starting calculation: Id={Id}, Iterations={Iterations}",
id, config.Iterations);
// ... calculation ...
_logger.LogInformation(
"[CALC] Completed in {ElapsedMs}ms",
stopwatch.ElapsedMilliseconds);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex,
"[ERROR] Calculation failed for analysis {Id}",
id);
throw;
}
}
}
Common Gotchas
Forgetting await
// WRONG: Returns Task instead of data
public async Task<IActionResult> Get()
{
var items = _db.Analyses.ToListAsync(); // Missing await!
return Ok(items);
}
// CORRECT
public async Task<IActionResult> Get()
{
var items = await _db.Analyses.ToListAsync();
return Ok(items);
}
Forgetting null checks
// WRONG: NullReferenceException if not found
public async Task<IActionResult> Get(Guid id)
{
var item = await _db.Analyses.FindAsync(id);
return Ok(item.Name); // Crashes if item is null!
}
// CORRECT
public async Task<IActionResult> Get(Guid id)
{
var item = await _db.Analyses.FindAsync(id);
if (item == null) return NotFound();
return Ok(item.Name);
}
Forgetting to save
// WRONG: Changes not persisted
public async Task<IActionResult> Update(Guid id, UpdateDto dto)
{
var item = await _db.Analyses.FindAsync(id);
item.Name = dto.Name;
// Missing SaveChangesAsync!
return Ok(item);
}
// CORRECT
public async Task<IActionResult> Update(Guid id, UpdateDto dto)
{
var item = await _db.Analyses.FindAsync(id);
item.Name = dto.Name;
await _db.SaveChangesAsync();
return Ok(item);
}
Forgetting authorization
// WRONG: Any user can access any analysis
public async Task<IActionResult> Get(Guid id)
{
var item = await _db.Analyses.FindAsync(id);
return Ok(item);
}
// CORRECT: Check ownership
public async Task<IActionResult> Get(Guid id)
{
var item = await _db.Analyses.FindAsync(id);
if (item == null) return NotFound();
if (item.UserId != GetCurrentUserId()) return Forbid();
return Ok(item);
}
File Locations
| What | Where | Layer |
|---|---|---|
| API Controllers | backend/src/WebApp.API/Controllers/ | API |
| Middleware | backend/src/WebApp.API/Middleware/ | API |
| App Entry Point | backend/src/WebApp.API/Program.cs | API |
| App Configuration | backend/src/WebApp.API/appsettings.json | API |
| Services | backend/src/WebApp.Application/Services/ | Application |
| Interfaces | backend/src/WebApp.Application/Interfaces/ | Application |
| DTOs (incl. ApiResponse) | backend/src/WebApp.Application/DTOs/ | Application |
| Domain Entities | backend/src/WebApp.Domain/Entities/ | Domain |
| Domain Enums | backend/src/WebApp.Domain/Enums/ | Domain |
| DbContext | backend/src/WebApp.Infrastructure/Data/AppDbContext.cs | Infrastructure |
| Entity Configurations | backend/src/WebApp.Infrastructure/Data/Configurations/ | Infrastructure |
| Repositories | backend/src/WebApp.Infrastructure/Repositories/ | Infrastructure |
| Typed HTTP Clients | backend/src/WebApp.Infrastructure/Clients/ | Infrastructure |