Lewati ke konten utama

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

  1. Jaga Program.cs tetap deklaratif — Semua logika startup harus berada dalam metode ekstensi. Program.cs harus dibaca seperti urutan langkah tingkat tinggi, tidak berisi detail implementasi.

  2. Daftarkan dependensi baru melalui metode ekstensi — Tambahkan layanan, opsi, dan pemeriksaan kesehatan baru di kelas ekstensi yang sesuai (ServiceCollectionExtensions, dll.).

  3. Gunakan typed options untuk konfigurasi — Jangan pernah membaca IConfiguration["key"] secara langsung dalam kode layanan atau controller. Alih-alih, definisikan kelas opsi di BackendConfiguration.cs dan suntikkan IOptions<T>.

  4. 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.

  5. Akses data dengan cakupan organisasi — Selalu batasi kueri data pada organisasi pengguna (dari klaim JWT) kecuali jika titik akhir tersebut secara eksplisit lintas-organisasi.

  6. 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 Asset dengan 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_at dan updated_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 ControllerBase secara 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

  1. Daftarkan di ServiceCollectionExtensions.cs:
    services.AddScoped<IAssetRepository, AssetRepository>();
    services.AddScoped<IAssetService, AssetService>();
  2. Buat dan jalankan migrasi:
    dotnet ef migrations add AddAssetEntity
    dotnet ef database update

Ringkasan — File yang Dibuat

LapisanFile
DomainDomain/Entities/Asset.cs
InfrastrukturInfrastructure/Data/ApplicationDbContext.cs (dimodifikasi)
AplikasiApplication/DTOs/AssetDtos.cs
InfrastrukturInfrastructure/Repositories/AssetRepository.cs
InfrastrukturInfrastructure/Services/AssetService.cs
APIAPI/Controllers/Assets/AssetsController.cs
EkstensiExtensions/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

ItemKonvensiContoh
Kelas entitasPascalCase tunggalSensor, VirtualSensor
Nama tabelhuruf kecil jamak/snakesensors, virtual_sensors
Nama kolomhuruf kecil snake_casesensor_ip, created_at
Primary keyidid
Foreign key{referenced}_idorganization_id
Kolom stempel waktucreated_at, updated_at
NamespaceRavenxcope.Backend.Domain.EntitiesSejajar dengan struktur folder

Penamaan DTO

ItemKonvensiContoh
DTO PembuatanCreate{Entity}RequestCreateSensorRequest
DTO PembaruanUpdate{Entity}RequestUpdateSensorRequest
DTO LoginNama deskriptifLoginRequest
DTO Metrik{Feature}MetricsDtoSensorHeartbeatMetricsDto
NamespaceRavenxcope.Backend.DTOs

Penamaan Controller

ItemKonvensiContoh
Kelas Controller{Feature}Controller (jamak)SensorsController, UsersController
Direktori ControllerControllers/{Feature}/Controllers/Sensors/
Ruteapi/{resource} (huruf kecil jamak)api/sensors, api/users
NamespaceRavenxcope.Backend.Controllers.{Feature}Ravenxcope.Backend.Controllers.Sensors

Penamaan Layanan

ItemKonvensiContoh
Kelas layanan{Name}ServiceJwtService, RedisService
AntarmukaI{Name}ServiceIOpenSearchService
NamespaceRavenxcope.Backend.Services

Konvensi Rute API

PolaMetodeContoh
api/{resource}GETapi/sensors (daftar semua)
api/{resource}/paginatedGETapi/users/paginated?page=1&pageSize=10
api/{resource}/{id}GETapi/sensors/{id} (ambil berdasarkan ID)
api/{resource}POSTapi/sensors (buat)
api/{resource}/{id}PUTapi/sensors/{id} (perbarui)
api/{resource}/{id}DELETEapi/sensors/{id} (hapus)

Catatan: Berbeda dengan konvensi boilerplate Sindika, proyek ini tidak menggunakan awalan versi v1 dalam 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

KategoriPemeriksaan
Builddotnet build berhasil tanpa kesalahan
StartupAplikasi berjalan sukses dengan konfigurasi pengembangan
KonfigurasiKunci wajib baru ditambahkan ke validasi
KonfigurasiKunci baru ditambahkan ke appsettings.json dengan placeholder
Migrasidotnet ef migrations add menghasilkan migrasi yang benar
Migrasidotnet ef database update diterapkan tanpa kesalahan
Titik AkhirTitik akhir baru mengembalikan kode status HTTP yang benar
Auth[Authorize] diterapkan ke titik akhir yang dilindungi
Scoping OrgKueri data memfilter berdasarkan organisasi dari klaim JWT
LoggingLogging terstruktur dengan konteks yang relevan
SwaggerTitik akhir baru muncul dengan benar di UI Swagger
DokumentasiBab 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:

  • AddAssetEntity
  • AddIpAddressToSensor
  • CreatePermissionRoleJunction
  • UpdateSensorStatusDefaults

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

GejalaKemungkinan PenyebabSolusi
Missing required configurationPlaceholder atau variabel lingk hilangAtur variabel lingk atau konf dev
Unable to connect to PostgreSQLDB tidak jalan atau kredensial salahVerifikasi string koneksi di konf dev
Redis connection failedRedis tidak jalanJalankan Redis, verifikasi host/port
InfluxDB health check returned 4xxURL salah, token salah, atau matiVerifikasi konf dan layanan InfluxDB
OpenSearch health check returned 4xxKredensial salah atau masalah SSLVerifikasi konf OpenSearch
JWT Secret must be at least 32 charactersRahasia terlalu pendekGunakan 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.