Panduan Pengembangan
Pendahuluan
Panduan ini mencakup prinsip-prinsip pengembangan, alur kerja langkah-demi-langkah untuk menambahkan fitur baru, manajemen konfigurasi, konvensi penamaan, dan standar pull request untuk backend Ravenxcope. Ikuti pola-pola ini untuk menjaga konsistensi di seluruh basis kode.
Prinsip Pengembangan
-
Jaga
Program.cstetap deklaratif — Semua logika startup harus berada dalam metode ekstensi.Program.csharus dibaca seperti urutan langkah tingkat tinggi, tidak berisi detail implementasi. -
Daftarkan dependensi baru melalui metode ekstensi — Tambahkan layanan, opsi, dan pemeriksaan kesehatan baru di kelas ekstensi yang sesuai (
ServiceCollectionExtensions, dll.). -
Gunakan typed options untuk konfigurasi — Jangan pernah membaca
IConfiguration["key"]secara langsung dalam kode layanan atau controller. Alih-alih, definisikan kelas opsi diBackendConfiguration.csdan suntikkanIOptions<T>. -
Utamakan layanan untuk logika bisnis — Jaga controller tetap tipis dengan hanya menangani masalah transport (permintaan HTTP → panggilan layanan → respons HTTP). Logika bisnis, validasi, dan orkestrasi harus berada di kelas layanan.
-
Akses data dengan cakupan organisasi — Selalu batasi kueri data pada organisasi pengguna (dari klaim JWT) kecuali jika titik akhir tersebut secara eksplisit lintas-organisasi.
-
Fail-fast untuk nilai wajib — Jika kunci konfigurasi bersifat wajib, tambahkan ke validasi startup. Jangan pernah diam-diam menggunakan nilai default untuk nilai yang kritis bagi produksi.
Langkah demi Langkah: Menambahkan Fitur CRUD Baru
Panduan ini mendemonstrasikan penambahan fitur baru yang lengkap ke backend, menggunakan entitas Asset sebagai contoh.
Skenario Contoh: Menambahkan entitas
Assetdengan titik akhir API CRUD lengkap.
Langkah 1: Definisikan Entitas
Buat kelas entitas di Domain/Entities/:
File: Domain/Entities/Asset.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Ravenxcope.Backend.Domain.Entities;
[Table("assets")]
public class Asset
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.CreateVersion7();
[Required]
[Column("name")]
[MaxLength(255)]
public string Name { get; set; } = string.Empty;
[Column("organization_id")]
public Guid OrganizationId { get; set; }
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Column("updated_at")]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Properti navigasi
[ForeignKey("OrganizationId")]
public Organization Organization { get; set; } = null!;
}
Konvensi kunci:
- Namespace:
Ravenxcope.Backend.Domain.Entities - Nama tabel: huruf kecil jamak (
assets) - Nama kolom: huruf kecil dengan garis bawah (
organization_id) - Gunakan
Guid.CreateVersion7()untuk UUID yang diurutkan berdasarkan waktu - Sertakan stempel waktu
created_atdanupdated_at
Langkah 2: Daftarkan di DbContext
File: Infrastructure/Data/ApplicationDbContext.cs
// 1. Tambahkan properti DbSet
public DbSet<Asset> Assets { get; set; }
// 2. Tambahkan konfigurasi di OnModelCreating
modelBuilder.Entity<Asset>(entity =>
{
entity.HasOne(a => a.Organization)
.WithMany()
.HasForeignKey(a => a.OrganizationId)
.OnDelete(DeleteBehavior.Cascade);
});
// 3. Tambahkan ke metode UpdateTimestamps
var entries = context.ChangeTracker.Entries()
.Where(e => e.Entity is User || e.Entity is Organization || /* ... */
e.Entity is Asset); // Tambahkan entitas baru di sini
Langkah 3: Buat DTO
Buat DTO permintaan/respons di Application/DTOs/:
File: Application/DTOs/AssetDtos.cs
namespace Ravenxcope.Backend.DTOs;
public sealed record AssetDto(
Guid Id,
string Name,
Guid OrganizationId,
DateTime CreatedAt,
DateTime UpdatedAt);
public class CreateAssetRequest
{
public string Name { get; set; } = string.Empty;
}
public class UpdateAssetRequest
{
public string? Name { get; set; }
}
Langkah 4: Buat Repositori
Buat repositori untuk menangani akses data.
File: Infrastructure/Repositories/AssetRepository.cs
using Ravenxcope.Backend.Data;
using Ravenxcope.Backend.Domain.Entities;
namespace Ravenxcope.Backend.Repositories;
public interface IAssetRepository : IRepository<Asset>
{
Task<IReadOnlyList<Asset>> GetByOrganizationIdAsync(Guid organizationId, CancellationToken cancellationToken = default);
}
public class AssetRepository : EfRepository<Asset>, IAssetRepository
{
public AssetRepository(ApplicationDbContext context) : base(context)
{
}
public async Task<IReadOnlyList<Asset>> GetByOrganizationIdAsync(Guid organizationId, CancellationToken cancellationToken = default)
{
return await _context.Assets
.Where(a => a.OrganizationId == organizationId)
.OrderBy(a => a.Name)
.ToListAsync(cancellationToken);
}
}
Langkah 5: Buat Layanan
Buat layanan untuk mengatur logika domain.
File: Infrastructure/Services/AssetService.cs
using Ravenxcope.Backend.DTOs;
using Ravenxcope.Backend.Repositories;
namespace Ravenxcope.Backend.Services;
public interface IAssetService
{
Task<IReadOnlyList<AssetDto>> GetAssetsAsync(Guid organizationId, CancellationToken cancellationToken = default);
}
public class AssetService : IAssetService
{
private readonly IAssetRepository _assetRepository;
public AssetService(IAssetRepository assetRepository)
{
_assetRepository = assetRepository;
}
public async Task<IReadOnlyList<AssetDto>> GetAssetsAsync(Guid organizationId, CancellationToken cancellationToken = default)
{
var assets = await _assetRepository.GetByOrganizationIdAsync(organizationId, cancellationToken);
return assets.Select(a => new AssetDto(a.Id, a.Name, a.OrganizationId, a.CreatedAt, a.UpdatedAt)).ToList();
}
}
Langkah 6: Buat Controller
File: API/Controllers/Assets/AssetsController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ravenxcope.Backend.DTOs;
using Ravenxcope.Backend.Services;
using Ravenxcope.Backend.Helpers;
namespace Ravenxcope.Backend.Controllers.Assets;
[ApiController]
[Route("api/assets")]
[Authorize]
public class AssetsController : ControllerBase
{
private readonly IAssetService _assetService;
public AssetsController(IAssetService assetService)
{
_assetService = assetService;
}
[HttpGet]
public async Task<IActionResult> GetAssets(CancellationToken cancellationToken)
{
var organizationId = UserHelper.GetOrganizationId(User);
if (!organizationId.HasValue)
{
return BadRequest(ApiEnvelope.Error("ID Organisasi tidak ditemukan dalam token", HttpContext.TraceIdentifier));
}
var assets = await _assetService.GetAssetsAsync(organizationId.Value, cancellationToken);
return Ok(ApiEnvelope.Success(new
{
items = assets,
count = assets.Count
}));
}
}
Konvensi controller:
- Dikelompokkan dalam subdirektori fitur (
Controllers/Assets/) - Rute:
api/{resource}(huruf kecil jamak, tanpa awalan versi) - Mewarisi
ControllerBasesecara langsung - Delegasikan semua logika ke layanan scoped (
IAssetService) - Kembalikan standar
ApiEnvelope.Success/ApiEnvelope.Error - Tanpa try-catch manual: Pengecualian ditangani oleh middleware pengecualian global.
Langkah 7: Daftarkan Layanan dan Migrasi
- Daftarkan di
ServiceCollectionExtensions.cs:services.AddScoped<IAssetRepository, AssetRepository>();services.AddScoped<IAssetService, AssetService>(); - Buat dan jalankan migrasi:
dotnet ef migrations add AddAssetEntitydotnet ef database update
Ringkasan — File yang Dibuat
| Lapisan | File |
|---|---|
| Domain | Domain/Entities/Asset.cs |
| Infrastruktur | Infrastructure/Data/ApplicationDbContext.cs (dimodifikasi) |
| Aplikasi | Application/DTOs/AssetDtos.cs |
| Infrastruktur | Infrastructure/Repositories/AssetRepository.cs |
| Infrastruktur | Infrastructure/Services/AssetService.cs |
| API | API/Controllers/Assets/AssetsController.cs |
| Ekstensi | Extensions/ServiceCollectionExtensions.cs (dimodifikasi) |
Langkah demi Langkah: Menambahkan Bagian Konfigurasi Baru
Ketika fitur baru membutuhkan konfigurasi:
Langkah 1: Definisikan Kelas Typed Options
File: Extensions/BackendConfiguration.cs
// 1. Tambahkan konstanta bagian
public const string MyFeatureSection = "MyFeature";
public const string MyFeatureApiUrlKey = "MyFeature:ApiUrl";
// 2. Tambahkan kelas opsi
public sealed class MyFeatureOptions
{
public string ApiUrl { get; set; } = string.Empty;
public int TimeoutSeconds { get; set; } = 30;
}
Langkah 2: Tambahkan ke appsettings.json
{
"MyFeature": {
"ApiUrl": "{{MyFeature__ApiUrl}}",
"TimeoutSeconds": 30
}
}
Langkah 3: Daftarkan Pengikatan Opsi (Options Binding)
File: Extensions/ServiceCollectionExtensions.cs
var myFeatureSection = configuration.GetSection(BackendConfigurationKeys.MyFeatureSection);
services.Configure<MyFeatureOptions>(myFeatureSection);
Langkah 4: Tambahkan Validasi Startup (jika wajib)
File: Extensions/ConfigurationValidationExtensions.cs
var requiredKeys = new[]
{
// ... kunci yang sudah ada ...
BackendConfigurationKeys.MyFeatureApiUrlKey, // Tambahkan di sini
};
Langkah 5: Suntikkan di Layanan
public class MyFeatureService
{
private readonly MyFeatureOptions _options;
public MyFeatureService(IOptions<MyFeatureOptions> options)
{
_options = options.Value;
}
}
Langkah demi Langkah: Menambahkan Layanan Infrastruktur Baru
Langkah 1: Buat Kelas Layanan
File: Infrastructure/Services/MyFeatureService.cs
using Microsoft.Extensions.Options;
using Ravenxcope.Backend.Extensions;
namespace Ravenxcope.Backend.Services;
public class MyFeatureService
{
private readonly MyFeatureOptions _options;
private readonly ILogger<MyFeatureService> _logger;
public MyFeatureService(
IOptions<MyFeatureOptions> options,
ILogger<MyFeatureService> logger)
{
_options = options.Value;
_logger = logger;
}
// Tambahkan metode layanan...
}
Langkah 2: Daftarkan Layanan
File: Extensions/ServiceCollectionExtensions.cs
// Pilih masa pakai (lifetime) berdasarkan penggunaan:
services.AddSingleton<MyFeatureService>(); // Stateless, thread-safe
services.AddScoped<MyFeatureService>(); // Status per permintaan
Langkah 3: Tambahkan Pemeriksaan Kesehatan (jika dependensi eksternal)
File: Extensions/StartupDependencyHealthChecksExtensions.cs
allPassed &= await HealthCheckHelper.RunHealthCheckWithRetry<IOptions<MyFeatureOptions>>(
services,
logger,
async (options, cancellationToken) =>
{
// Implementasikan pemeriksaan konektivitas
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync(options.Value.ApiUrl, cancellationToken);
response.EnsureSuccessStatusCode();
},
"MyFeature");
Konvensi Penamaan
Penamaan Entitas
| Item | Konvensi | Contoh |
|---|---|---|
| Kelas entitas | PascalCase tunggal | Sensor, VirtualSensor |
| Nama tabel | huruf kecil jamak/snake | sensors, virtual_sensors |
| Nama kolom | huruf kecil snake_case | sensor_ip, created_at |
| Primary key | id | id |
| Foreign key | {referenced}_id | organization_id |
| Kolom stempel waktu | created_at, updated_at | — |
| Namespace | Ravenxcope.Backend.Domain.Entities | Sejajar dengan struktur folder |
Penamaan DTO
| Item | Konvensi | Contoh |
|---|---|---|
| DTO Pembuatan | Create{Entity}Request | CreateSensorRequest |
| DTO Pembaruan | Update{Entity}Request | UpdateSensorRequest |
| DTO Login | Nama deskriptif | LoginRequest |
| DTO Metrik | {Feature}MetricsDto | SensorHeartbeatMetricsDto |
| Namespace | Ravenxcope.Backend.DTOs | — |
Penamaan Controller
| Item | Konvensi | Contoh |
|---|---|---|
| Kelas Controller | {Feature}Controller (jamak) | SensorsController, UsersController |
| Direktori Controller | Controllers/{Feature}/ | Controllers/Sensors/ |
| Rute | api/{resource} (huruf kecil jamak) | api/sensors, api/users |
| Namespace | Ravenxcope.Backend.Controllers.{Feature} | Ravenxcope.Backend.Controllers.Sensors |
Penamaan Layanan
| Item | Konvensi | Contoh |
|---|---|---|
| Kelas layanan | {Name}Service | JwtService, RedisService |
| Antarmuka | I{Name}Service | IOpenSearchService |
| Namespace | Ravenxcope.Backend.Services | — |
Konvensi Rute API
| Pola | Metode | Contoh |
|---|---|---|
api/{resource} | GET | api/sensors (daftar semua) |
api/{resource}/paginated | GET | api/users/paginated?page=1&pageSize=10 |
api/{resource}/{id} | GET | api/sensors/{id} (ambil berdasarkan ID) |
api/{resource} | POST | api/sensors (buat) |
api/{resource}/{id} | PUT | api/sensors/{id} (perbarui) |
api/{resource}/{id} | DELETE | api/sensors/{id} (hapus) |
Catatan: Berbeda dengan konvensi boilerplate Sindika, proyek ini tidak menggunakan awalan versi
v1dalam rute dan menggunakan GET (bukan POST) untuk paginasi.
Alur Kerja Konfigurasi
Menambahkan Kunci Konfigurasi Wajib
1. Definisikan konstanta di BackendConfigurationKeys
→ Extensions/BackendConfiguration.cs
2. Definisikan kelas opsi ber-tipe (jika bagian baru)
→ Extensions/BackendConfiguration.cs
3. Tambahkan ke appsettings.json dengan nilai placeholder
→ appsettings.json
4. Tambahkan nilai aktual ke konfigurasi pengembangan
→ appsettings.Development.json
5. Daftarkan pengikatan opsi dalam registrasi layanan
→ Extensions/ServiceCollectionExtensions.cs
6. Tambahkan ke validasi kunci wajib
→ Extensions/ConfigurationValidationExtensions.cs
7. Tambahkan validasi integer positif jika berlaku
→ Extensions/ConfigurationValidationExtensions.cs
8. Tambahkan ke skrip preflight jika kritis bagi penyebaran
→ defense_center/scripts/preflight-env.sh
Daftar Periksa Pull Request
| Kategori | Pemeriksaan |
|---|---|
| Build | dotnet build berhasil tanpa kesalahan |
| Startup | Aplikasi berjalan sukses dengan konfigurasi pengembangan |
| Konfigurasi | Kunci wajib baru ditambahkan ke validasi |
| Konfigurasi | Kunci baru ditambahkan ke appsettings.json dengan placeholder |
| Migrasi | dotnet ef migrations add menghasilkan migrasi yang benar |
| Migrasi | dotnet ef database update diterapkan tanpa kesalahan |
| Titik Akhir | Titik akhir baru mengembalikan kode status HTTP yang benar |
| Auth | [Authorize] diterapkan ke titik akhir yang dilindungi |
| Scoping Org | Kueri data memfilter berdasarkan organisasi dari klaim JWT |
| Logging | Logging terstruktur dengan konteks yang relevan |
| Swagger | Titik akhir baru muncul dengan benar di UI Swagger |
| Dokumentasi | Bab Pengetahuan (Knowledge) diperbarui jika berlaku |
Anti-Pola Umum yang Harus Dihindari
Hindari pembacaan konfigurasi langsung di controller
// BURUK — pembacaan IConfiguration yang tersebar
var url = _configuration["AnsibleSettings:ServiceUrl"];
// BAIK — injeksi typed options
public class MyService(IOptions<AnsibleSettingsOptions> options)
{
private readonly string _url = options.Value.ServiceUrl;
}
Hindari logika bisnis di controller
// BURUK — logika orkestrasi di controller
[HttpPost("activate")]
public async Task<IActionResult> Activate(Guid id)
{
var sensor = await _context.Sensors.FindAsync(id);
// ... 50 baris logika bisnis ...
// ... beberapa panggilan layanan ...
// ... penanganan kesalahan ...
}
// BAIK — delegasikan ke layanan
[HttpPost("activate")]
public async Task<IActionResult> Activate(Guid id)
{
var result = await _sensorsService.ActivateSensorAsync(id, request);
return Ok(ApiEnvelope.Success(result));
}
Hindari fallback diam-diam untuk rahasia wajib
// BURUK — menggunakan default secara diam-diam
var secret = configuration["JwtSettings:Secret"] ?? "default-secret";
// BAIK — fail-fast
var secret = configuration["JwtSettings:Secret"]
?? throw new InvalidOperationException("Rahasia JWT tidak dikonfigurasi");
Hindari bentuk respons yang tidak konsisten
// BURUK — bentuk berbeda per titik akhir
return Ok(users); // Titik akhir A
return Ok(new { data = users }); // Titik akhir B
return Ok(new { success = true, data = users }); // Titik akhir C
// BAIK — amplop yang konsisten
return Ok(ApiEnvelope.Success(new { items = users, count = users.Count }));
Hindari hapus permanen (hard delete) tanpa pertimbangan
// WASPADA — beberapa entitas menggunakan hard delete, verifikasi niat
_context.Users.Remove(user); // Permanen, menghapus rekaman terkait secara cascade
Panduan Migrasi EF Core
Membuat Migrasi
dotnet ef migrations add {NamaDeskriptif}
Nama migrasi yang baik:
AddAssetEntityAddIpAddressToSensorCreatePermissionRoleJunctionUpdateSensorStatusDefaults
Menerapkan Migrasi
# Terapkan semua migrasi yang tertunda
dotnet ef database update
# Terapkan hingga migrasi tertentu
dotnet ef database update {NamaMigrasi}
Menghapus Migrasi Terakhir
dotnet ef migrations remove
Migrasi Otomatis saat Startup
Aplikasi secara otomatis menerapkan migrasi yang tertunda saat startup ketika Database:AutoMigrate bernilai true (default). Nonaktifkan untuk lingkungan produksi di mana migrasi harus diterapkan secara manual:
{
"Database": {
"AutoMigrate": false
}
}
Tip Debugging
Diagnosis Kegagalan Startup
| Gejala | Kemungkinan Penyebab | Solusi |
|---|---|---|
Missing required configuration | Placeholder atau variabel lingk hilang | Atur variabel lingk atau konf dev |
Unable to connect to PostgreSQL | DB tidak jalan atau kredensial salah | Verifikasi string koneksi di konf dev |
Redis connection failed | Redis tidak jalan | Jalankan Redis, verifikasi host/port |
InfluxDB health check returned 4xx | URL salah, token salah, atau mati | Verifikasi konf dan layanan InfluxDB |
OpenSearch health check returned 4xx | Kredensial salah atau masalah SSL | Verifikasi konf OpenSearch |
JWT Secret must be at least 32 characters | Rahasia terlalu pendek | Gunakan rahasia lebih panjang di konf dev |
Penimpaan Serilog yang Berguna untuk Pengembangan
{
"Serilog": {
"MinimumLevel": {
"Override": {
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
}
Ini mengaktifkan logging kueri SQL EF Core untuk men-debug masalah database.