Octopus
Plugin Configuration
Plugins read configuration from appsettings.json via the OctopusConfig object passed to OnRegister. This page covers binding config sections to typed options classes, using the IOptions pattern, environment overrides, and secrets management.
Configuration Entry Point
The OctopusConfig object wraps the application's IConfiguration and provides a helper method for binding plugin-specific sections:
public void OnRegister(IServiceCollection services, OctopusConfig config)
{
// Method 1: Use the OctopusConfig helper (binds and returns a new instance)
var pluginConfig = config.GetSection<HRPluginConfig>("HRPlugin");
// Method 2: Bind directly to IConfiguration (same result)
var pluginConfig2 = config.Configuration
.GetSection("HRPlugin")
.Get<HRPluginConfig>()!;
// Method 3: Register for IOptions<T> injection (recommended for services)
services.Configure<HRPluginConfig>(
config.Configuration.GetSection("HRPlugin"));
}
Defining a Plugin Options Class
namespace BizFirst.Octopus.Plugins.HR;
public class HRPluginConfig
{
// Non-nullable strings should have sensible defaults or be validated
public string BaseUrl { get; set; } = string.Empty;
public string ApiVersion { get; set; } = "v2";
public int TimeoutSeconds { get; set; } = 30;
public int MaxRetries { get; set; } = 3;
public bool EnableCaching { get; set; } = true;
public int CacheTtlMinutes { get; set; } = 15;
// Credential IDs — never store raw secrets in config
public int ApiKeyCredentialId { get; set; } // resolved via ICredentialResolver
// Nested objects for logical grouping
public HRCachingConfig Caching { get; set; } = new();
public HRFeaturesConfig Features { get; set; } = new();
}
public class HRCachingConfig
{
public bool Enabled { get; set; } = true;
public int TtlMinutes { get; set; } = 15;
public int MaxEntries { get; set; } = 1000;
public string BackingStore { get; set; } = "Memory"; // Memory | Redis
}
public class HRFeaturesConfig
{
public bool LeaveManagement { get; set; } = true;
public bool Payroll { get; set; } = false;
public bool PerformanceReview { get; set; } = false;
}
appsettings.json Structure
{
"HRPlugin": {
"BaseUrl": "https://hr-api.internal.company.com",
"ApiVersion": "v2",
"TimeoutSeconds": 30,
"MaxRetries": 3,
"ApiKeyCredentialId": 42,
"Caching": {
"Enabled": true,
"TtlMinutes": 15,
"MaxEntries": 1000,
"BackingStore": "Memory"
},
"Features": {
"LeaveManagement": true,
"Payroll": false,
"PerformanceReview": false
}
}
}
Consuming Config in Services via IOptions
// Service that receives config via constructor injection
public class HRSystemClient : IHRSystemClient
{
private readonly HRPluginConfig _config;
private readonly ICredentialResolver _creds;
private readonly HttpClient _http;
public HRSystemClient(
IOptions<HRPluginConfig> options,
ICredentialResolver credentialResolver,
HttpClient http)
{
_config = options.Value;
_creds = credentialResolver;
_http = http;
_http.BaseAddress = new Uri(_config.BaseUrl);
_http.Timeout = TimeSpan.FromSeconds(_config.TimeoutSeconds);
}
public async Task<LeaveBalance> GetLeaveBalanceAsync(string employeeId, CancellationToken ct)
{
// Resolve the API key from the credential store (never from config directly)
var apiKey = await _creds.GetPasswordAsync(_config.ApiKeyCredentialId);
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", apiKey);
var response = await _http.GetAsync($"/employees/{employeeId}/leave", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LeaveBalance>(ct)!;
}
}
Environment-Specific Overrides
ASP.NET Core's standard layered configuration applies. Octopus reads from all configured sources in order:
| Source | Priority | Use For |
|---|---|---|
appsettings.json | Base (lowest) | Default values, non-sensitive config |
appsettings.{Environment}.json | Environment | Dev/staging/production overrides |
| Environment variables | High | Container/cloud deployments |
| Azure Key Vault / AWS Secrets | High | Production secrets (base URLs, etc.) |
| Command-line arguments | Highest | One-off overrides, testing |
// Environment variable format: double underscore separates nesting levels
// HRPlugin__BaseUrl=https://prod-hr.company.com
// HRPlugin__Caching__TtlMinutes=30
// HRPlugin__Features__Payroll=true
// appsettings.Production.json
{
"HRPlugin": {
"BaseUrl": "https://prod-hr.company.com",
"Caching": {
"BackingStore": "Redis"
}
}
}
Config Validation at Startup
Use data annotations or IValidateOptions to fail fast if required config is missing:
using System.ComponentModel.DataAnnotations;
public class HRPluginConfig
{
[Required, Url]
public string BaseUrl { get; set; } = string.Empty;
[Range(1, 300)]
public int TimeoutSeconds { get; set; } = 30;
[Range(1, int.MaxValue, ErrorMessage = "ApiKeyCredentialId must be a valid credential ID")]
public int ApiKeyCredentialId { get; set; }
}
// In OnRegister — register with validation
public void OnRegister(IServiceCollection services, OctopusConfig config)
{
services.AddOptions<HRPluginConfig>()
.Bind(config.Configuration.GetSection("HRPlugin"))
.ValidateDataAnnotations()
.ValidateOnStart(); // Fails startup immediately if config is invalid
}
ValidateOnStart — This causes the application to throw on startup rather than failing at the first request that uses the bad config. Always use this for production plugins.
Secrets and Credentials
Never store raw secrets in appsettings.json. API keys, passwords, and tokens must be stored in the credential store and referenced by integer credential ID only. The credential resolver fetches secrets at runtime.
// WRONG — never do this
{
"HRPlugin": {
"ApiKey": "sk-prod-abc123..." // ❌ Exposed in config files and logs
}
}
// CORRECT — store the credential ID only
{
"HRPlugin": {
"ApiKeyCredentialId": 42 // ✓ The actual key lives in the credential store
}
}
// At runtime:
var apiKey = await credentialResolver.GetPasswordAsync(config.ApiKeyCredentialId);