FluentInput 0.1.0

dotnet add package FluentInput --version 0.1.0
                    
NuGet\Install-Package FluentInput -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FluentInput" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FluentInput" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="FluentInput" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FluentInput --version 0.1.0
                    
#r "nuget: FluentInput, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FluentInput@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FluentInput&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=FluentInput&version=0.1.0
                    
Install as a Cake Tool

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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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