FluentInput 0.1.0
dotnet add package FluentInput --version 0.1.0
NuGet\Install-Package FluentInput -Version 0.1.0
<PackageReference Include="FluentInput" Version="0.1.0" />
<PackageVersion Include="FluentInput" Version="0.1.0" />
<PackageReference Include="FluentInput" />
paket add FluentInput --version 0.1.0
#r "nuget: FluentInput, 0.1.0"
#:package FluentInput@0.1.0
#addin nuget:?package=FluentInput&version=0.1.0
#tool nuget:?package=FluentInput&version=0.1.0
Notice
This library is initially written by an AI. Some changes have been made by a human. Overall, the project looks solid as it is but the API is subject to changes.
FluentInput
A C# library for declarative, dynamic configuration with runtime schema discovery. Inspired by TradingView's PineScript input() function.
Overview
FluentInput provides a unique approach to configuration where the schema is discovered by executing the configuration code. This enables:
- Conditional inputs that appear/disappear based on other values
- Polymorphic type selection at runtime
- Composable configurations that can depend on other configurations
- Automatic UI generation from the discovered schema
public static MyStrategy Create(IInputProvider input)
{
var sensitivity = input.Int("sensitivity", "Sensitivity", defaultVal: 3, minVal: 1, maxVal: 10);
var enableAlerts = input.Bool("enableAlerts", "Enable Alerts");
if (enableAlerts)
{
// This input only appears when alerts are enabled
var email = input.String("email", "Alert Email", required: true);
}
// User selects which strategy implementation to use
var strategy = input.PolymorphicDependency<IStrategy>("strategy", "Strategy Type");
return new MyStrategy(sensitivity, strategy);
}
Features
| Feature | Description |
|---|---|
| Dynamic Schema | Schema is built as configuration code executes |
| Conditional Inputs | Inputs appear/disappear based on runtime conditions |
| Polymorphic Dependencies | Select implementation type from registered options |
| Nested Configurations | Configurations can depend on other configurations |
| Validation | Built-in and custom validation with detailed errors |
| DI Integration | First-class Microsoft.Extensions.DependencyInjection support |
| Type Safety | Full compile-time type checking |
| UI Generation | Schema output suitable for generating dynamic UIs |
Installation
dotnet add package FluentInput
Quick Start
1. Define a Configurable Type
public class MySettings : IConfigurable<MySettings>
{
public static string DisplayName => "My Settings";
public int Threshold { get; }
public string Name { get; }
private MySettings(int threshold, string name)
{
Threshold = threshold;
Name = name;
}
public static MySettings Create(IInputProvider input)
{
var threshold = input.Int("threshold", "Threshold", defaultVal: 50, minVal: 0, maxVal: 100);
var name = input.String("name", "Name", required: true);
return new MySettings(threshold, name);
}
}
2. Register Services
var services = new ServiceCollection();
services.AddFluentInput();
var provider = services.BuildServiceProvider();
3. Create Configuration
var configManager = provider.GetRequiredService<IConfigurationManager>();
// With defaults
var result = configManager.Create<MySettings>();
// With user values
var values = new Dictionary<string, object>
{
["threshold"] = 75,
["name"] = "Production"
};
var result = configManager.Create<MySettings>(values);
if (result.IsValid)
{
var settings = result.Instance;
Console.WriteLine($"Threshold: {settings.Threshold}");
}
Core Concepts
IConfigurable<T>
The base interface for all configurable types:
public interface IConfigurable<TSelf> where TSelf : IConfigurable<TSelf>
{
static abstract TSelf Create(IInputProvider input);
static abstract string DisplayName { get; }
}
IInputProvider
Provides methods to declare and retrieve inputs:
public interface IInputProvider
{
// Primitives
int Int(string key, string label, int defaultVal = 0, int? minVal = null, int? maxVal = null);
double Double(string key, string label, double defaultVal = 0, double? minVal = null, double? maxVal = null);
string String(string key, string label, string defaultVal = "", bool required = false, bool secret = false);
bool Bool(string key, string label, bool defaultVal = false);
T Enum<T>(string key, string label, T defaultVal = default) where T : struct, System.Enum;
// Grouping
IInputProvider Group(string groupKey, string groupLabel);
// Dependencies
T Dependency<T>(string key, string label) where T : IConfigurable<T>;
T? OptionalDependency<T>(string key, string label, bool defaultEnabled = false) where T : IConfigurable<T>;
IList<T> DependencyList<T>(string key, string label, int minCount = 0, int maxCount = 10) where T : IConfigurable<T>;
// Polymorphic Dependencies
TInterface PolymorphicDependency<TInterface>(string key, string label) where TInterface : class;
TInterface? OptionalPolymorphicDependency<TInterface>(string key, string label, bool defaultEnabled = false) where TInterface : class;
IList<TInterface> PolymorphicDependencyList<TInterface>(string key, string label, int minCount = 0, int maxCount = 10) where TInterface : class;
}
ConfigurationResult<T>
The result of creating a configuration:
public record ConfigurationResult<T>(
T? Instance, // The created instance (null if invalid)
List<InputDefinition> AccessedInputs, // Schema of accessed inputs
List<ValidationError> Errors // Validation errors
)
{
public bool IsValid => !Errors.Any() && Instance is not null;
}
Input Types
Primitive Inputs
// Integer with range validation
var count = input.Int("count", "Count", defaultVal: 10, minVal: 1, maxVal: 100);
// Double with range validation
var rate = input.Double("rate", "Rate", defaultVal: 0.5, minVal: 0.0, maxVal: 1.0);
// String (optional)
var description = input.String("description", "Description", defaultVal: "");
// String (required)
var name = input.String("name", "Name", required: true);
// String (secret - for passwords, API keys)
var apiKey = input.String("apiKey", "API Key", required: true, secret: true);
// Boolean
var enabled = input.Bool("enabled", "Enabled", defaultVal: true);
// Enum
var mode = input.Enum("mode", "Mode", MyEnum.Default);
Grouped Inputs
var advancedGroup = input.Group("advanced", "Advanced Settings");
var timeout = advancedGroup.Int("timeout", "Timeout (ms)", defaultVal: 5000);
var retries = advancedGroup.Int("retries", "Max Retries", defaultVal: 3);
// Keys become: "advanced.timeout", "advanced.retries"
Conditional Inputs
var useProxy = input.Bool("useProxy", "Use Proxy");
if (useProxy)
{
// These inputs only appear in schema when useProxy is true
var proxyHost = input.String("proxyHost", "Proxy Host", required: true);
var proxyPort = input.Int("proxyPort", "Proxy Port", defaultVal: 8080);
}
Concrete Dependencies
public class OuterConfig : IConfigurable<OuterConfig>
{
public static string DisplayName => "Outer Config";
public InnerConfig Inner { get; }
private OuterConfig(InnerConfig inner) => Inner = inner;
public static OuterConfig Create(IInputProvider input)
{
// Always includes InnerConfig
var inner = input.Dependency<InnerConfig>("inner", "Inner Settings");
return new OuterConfig(inner);
}
}
Optional Dependencies
// Optional nested config with enable toggle
var advanced = input.OptionalDependency<AdvancedConfig>("advanced", "Advanced Settings");
if (advanced != null)
{
// Use advanced settings
}
Dependency Lists
// List of 0-5 items
var filters = input.DependencyList<FilterConfig>("filters", "Filters", minCount: 0, maxCount: 5);
foreach (var filter in filters)
{
// Use each filter
}
Polymorphic Dependencies
Polymorphic dependencies allow users to select which implementation to use from a dropdown.
1. Define an Interface
public interface INotificationProvider
{
string Name { get; }
Task SendAsync(string message);
}
2. Implement Multiple Options
public class EmailNotification : IConfigurable<EmailNotification>, INotificationProvider
{
public static string DisplayName => "Email";
public string Name => DisplayName;
public string Address { get; }
private EmailNotification(string address) => Address = address;
public static EmailNotification Create(IInputProvider input)
{
var address = input.String("address", "Email Address", required: true);
return new EmailNotification(address);
}
public Task SendAsync(string message) => /* ... */;
}
public class SlackNotification : IConfigurable<SlackNotification>, INotificationProvider
{
public static string DisplayName => "Slack";
public string Name => DisplayName;
public string WebhookUrl { get; }
public string Channel { get; }
private SlackNotification(string webhookUrl, string channel)
{
WebhookUrl = webhookUrl;
Channel = channel;
}
public static SlackNotification Create(IInputProvider input)
{
var webhookUrl = input.String("webhookUrl", "Webhook URL", required: true);
var channel = input.String("channel", "Channel", defaultVal: "#general");
return new SlackNotification(webhookUrl, channel);
}
public Task SendAsync(string message) => /* ... */;
}
3. Register Implementations
services.AddFluentInput();
services.AddConfigurable<INotificationProvider, EmailNotification>();
services.AddConfigurable<INotificationProvider, SlackNotification>();
4. Use Polymorphic Dependency
public class AlertConfig : IConfigurable<AlertConfig>
{
public static string DisplayName => "Alert Configuration";
public INotificationProvider Primary { get; }
public INotificationProvider? Backup { get; }
public IList<INotificationProvider> Additional { get; }
private AlertConfig(
INotificationProvider primary,
INotificationProvider? backup,
IList<INotificationProvider> additional)
{
Primary = primary;
Backup = backup;
Additional = additional;
}
public static AlertConfig Create(IInputProvider input)
{
// Required - user selects Email or Slack
var primary = input.PolymorphicDependency<INotificationProvider>("primary", "Primary Notification");
// Optional with enable toggle
var backup = input.OptionalPolymorphicDependency<INotificationProvider>("backup", "Backup Notification");
// List of 0-3 additional providers
var additional = input.PolymorphicDependencyList<INotificationProvider>("additional", "Additional", maxCount: 3);
return new AlertConfig(primary, backup, additional);
}
}
5. Provide Values
var values = new Dictionary<string, object>
{
["primary.__type"] = "Slack", // Select Slack implementation
["primary.webhookUrl"] = "https://...",
["primary.channel"] = "#alerts",
["backup.__enabled"] = true, // Enable backup
["backup.__type"] = "Email", // Select Email implementation
["backup.address"] = "alerts@example.com",
["additional.__count"] = 1, // One additional provider
["additional[0].__type"] = "Email",
["additional[0].address"] = "backup@example.com"
};
var result = configManager.Create<AlertConfig>(values);
Validation
Built-in Validation
// Range validation
var value = input.Int("value", "Value", minVal: 1, maxVal: 100);
// Required validation
var name = input.String("name", "Name", required: true);
Custom Validation
public static MyConfig Create(IInputProvider input)
{
var email = input.String("email", "Email", required: true);
if (!email.Contains('@'))
throw new InputValidationException("email", "Invalid email format");
var startDate = input.String("startDate", "Start Date", required: true);
var endDate = input.String("endDate", "End Date", required: true);
if (DateTime.Parse(endDate) <= DateTime.Parse(startDate))
throw new InputValidationException("endDate", "End date must be after start date");
return new MyConfig(email, startDate, endDate);
}
Handling Validation Errors
var result = configManager.Create<MyConfig>(values);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"{error.Key}: {error.Message}");
}
}
Schema Discovery
The schema is automatically discovered when configuration code executes:
var result = configManager.Create<MyConfig>(values);
foreach (var input in result.AccessedInputs)
{
Console.WriteLine($"Key: {input.Key}");
Console.WriteLine($"Label: {input.Label}");
Console.WriteLine($"Type: {input.Type}");
Console.WriteLine($"Default: {input.DefaultValue}");
Console.WriteLine($"Required: {input.Required}");
Console.WriteLine($"Min: {input.MinValue}");
Console.WriteLine($"Max: {input.MaxValue}");
if (input.AvailableTypes != null)
{
Console.WriteLine($"Options: {string.Join(", ", input.AvailableTypes.Select(t => t.DisplayName))}");
}
if (input.ChildInputs != null)
{
// Recursively process nested inputs
}
}
InputDefinition Properties
| Property | Type | Description |
|---|---|---|
Key |
string |
Unique key for the input |
Label |
string |
Human-readable label |
Type |
InputType |
Type of input (Int, String, Bool, etc.) |
DefaultValue |
object? |
Default value |
MinValue |
object? |
Minimum value (for numeric types) |
MaxValue |
object? |
Maximum value (for numeric types) |
Required |
bool |
Whether the input is required |
Secret |
bool |
Whether the input should be masked |
EnumType |
Type? |
Enum type (for Enum inputs) |
DependencyType |
Type? |
Interface/class type (for dependencies) |
ChildInputs |
List<InputDefinition>? |
Nested inputs |
AvailableTypes |
IReadOnlyList<PolymorphicOption>? |
Available implementations |
SelectedTypeName |
string? |
Currently selected type |
UI Generation Example
Use the schema to generate dynamic UIs:
Blazor Example
@foreach (var input in Schema)
{
<div class="form-group">
<label>@input.Label</label>
@switch (input.Type)
{
case InputType.Int:
<input type="number"
min="@input.MinValue"
max="@input.MaxValue"
value="@GetValue(input.Key, input.DefaultValue)"
@onchange="e => SetValue(input.Key, e.Value)" />
break;
case InputType.String:
<input type="@(input.Secret ? "password" : "text")"
value="@GetValue(input.Key, input.DefaultValue)"
@onchange="e => SetValue(input.Key, e.Value)" />
break;
case InputType.Bool:
<input type="checkbox"
checked="@GetValue(input.Key, input.DefaultValue)"
@onchange="e => SetValue(input.Key, e.Value)" />
break;
case InputType.Enum:
<select @onchange="e => SetValue(input.Key, e.Value)">
@foreach (var option in Enum.GetValues(input.EnumType!))
{
<option value="@option">@option</option>
}
</select>
break;
case InputType.PolymorphicDependency:
<select @onchange="e => SetTypeAndRefresh(input.Key, e.Value)">
@foreach (var option in input.AvailableTypes!)
{
<option value="@option.DisplayName">@option.DisplayName</option>
}
</select>
@if (input.ChildInputs != null)
{
<div class="nested">
@RenderInputs(input.ChildInputs)
</div>
}
break;
}
</div>
}
@code {
private void SetTypeAndRefresh(string key, object? value)
{
SetValue($"{key}.__type", value);
RefreshSchema(); // Re-run to get new child inputs
}
}
JSON Schema Export
public static JsonObject ToJsonSchema(IReadOnlyList<InputDefinition> inputs)
{
var properties = new JsonObject();
var required = new JsonArray();
foreach (var input in inputs)
{
var prop = new JsonObject
{
["title"] = input.Label,
["type"] = input.Type switch
{
InputType.Int => "integer",
InputType.Double => "number",
InputType.Bool => "boolean",
_ => "string"
}
};
if (input.DefaultValue != null)
prop["default"] = JsonValue.Create(input.DefaultValue);
if (input.MinValue != null)
prop["minimum"] = JsonValue.Create(input.MinValue);
if (input.MaxValue != null)
prop["maximum"] = JsonValue.Create(input.MaxValue);
properties[input.Key] = prop;
if (input.Required)
required.Add(input.Key);
}
return new JsonObject
{
["type"] = "object",
["properties"] = properties,
["required"] = required
};
}
ASP.NET Core Integration
Controller Example
[ApiController]
[Route("api/config")]
public class ConfigController : ControllerBase
{
private readonly IConfigurationManager _configManager;
public ConfigController(IConfigurationManager configManager)
{
_configManager = configManager;
}
[HttpGet("schema")]
public IActionResult GetSchema()
{
var result = _configManager.Create<MyConfig>();
return Ok(result.AccessedInputs);
}
[HttpPost("validate")]
public IActionResult Validate([FromBody] Dictionary<string, object> values)
{
var result = _configManager.Create<MyConfig>(values);
return Ok(new
{
result.IsValid,
result.Errors,
Schema = result.AccessedInputs
});
}
[HttpPost]
public IActionResult Create([FromBody] Dictionary<string, object> values)
{
var result = _configManager.Create<MyConfig>(values);
if (!result.IsValid)
return BadRequest(result.Errors);
// Use result.Instance
return Ok();
}
}
Startup Registration
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFluentInput();
builder.Services.AddConfigurable<IStrategy, SimpleStrategy>();
builder.Services.AddConfigurable<IStrategy, AdvancedStrategy>();
builder.Services.AddConfigurable<INotification, EmailNotification>();
builder.Services.AddConfigurable<INotification, SlackNotification>();
var app = builder.Build();
API Reference
Service Registration
// Add core services
services.AddFluentInput();
// Register polymorphic implementations
services.AddConfigurable<TInterface, TImplementation>();
IConfigurationManager
public interface IConfigurationManager
{
ConfigurationResult<T> Create<T>(Dictionary<string, object>? values = null)
where T : IConfigurable<T>;
}
Extension Methods
// Get all registered implementations for an interface
IReadOnlyList<PolymorphicOption> types = serviceProvider.GetConfigurableTypes<IMyInterface>();
Special Keys
| Key Pattern | Purpose |
|---|---|
key.__type |
Select polymorphic implementation type |
key.__enabled |
Enable/disable optional dependency |
key.__count |
Number of items in dependency list |
key[0], key[1], ... |
List item values |
group.key |
Grouped input key |
Comparison with Alternatives
| Feature | FluentInput | MS Options | FluentValidation | System.CommandLine |
|---|---|---|---|---|
| Dynamic schema | ✅ | ❌ | ❌ | ✅ |
| Conditional inputs | ✅ | ❌ | ❌ | ⚠️ |
| Polymorphic types | ✅ | ❌ | ❌ | ❌ |
| Nested configs | ✅ | ⚠️ | ✅ | ❌ |
| Runtime validation | ✅ | ✅ | ✅ | ✅ |
| UI generation | ✅ | ❌ | ❌ | ✅ (CLI) |
| DI integration | ✅ | ✅ | ✅ | ✅ |
License
MIT License
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 118 | 2/4/2026 |