FluentSignals 2.1.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package FluentSignals --version 2.1.1
                    
NuGet\Install-Package FluentSignals -Version 2.1.1
                    
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="FluentSignals" Version="2.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FluentSignals" Version="2.1.1" />
                    
Directory.Packages.props
<PackageReference Include="FluentSignals" />
                    
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 FluentSignals --version 2.1.1
                    
#r "nuget: FluentSignals, 2.1.1"
                    
#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 FluentSignals@2.1.1
                    
#: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=FluentSignals&version=2.1.1
                    
Install as a Cake Addin
#tool nuget:?package=FluentSignals&version=2.1.1
                    
Install as a Cake Tool

FluentSignals

A powerful reactive state management library for .NET applications inspired by SolidJS signals. FluentSignals provides fine-grained reactivity with automatic dependency tracking, making it perfect for building responsive applications with minimal boilerplate.

📦 Latest Version: 1.1.2

What's New in 1.1.2

  • Custom JSON Serialization: Configure JsonSerializerOptions for HttpResource
  • Bug Fix: Resolved duplicate handler calls for typed HTTP status handlers
  • Enhanced Documentation: Added comprehensive guides for JSON deserialization

See the full changelog for version history.

Features

  • 🚀 Fine-grained reactivity - Only update what needs to be updated
  • 🔄 Automatic dependency tracking - No need to manually manage subscriptions
  • 📦 Typed and untyped signals - Use Signal<T> for type safety or Signal for flexibility
  • Async signals - Built-in support for asynchronous operations
  • 🌊 Computed signals - Automatically derive values from other signals
  • 🎯 Resource management - Generic resource pattern with loading/error states
  • 🌐 HTTP resources - Built-in HTTP client with caching and retry policies
  • 🔌 Extensible - Easy to extend with custom signal types

Installation

dotnet add package FluentSignals

Quick Start

Basic Signal Usage

using FluentSignals;

// Create a signal
var count = new Signal<int>(0);

// Subscribe to changes
count.Subscribe(value => Console.WriteLine($"Count is now: {value}"));

// Update the signal
count.Value = 1; // Output: Count is now: 1
count.Value = 2; // Output: Count is now: 2

Computed Signals

var firstName = new Signal<string>("John");
var lastName = new Signal<string>("Doe");

// Create a computed signal
var fullName = new ComputedSignal<string>(() => $"{firstName.Value} {lastName.Value}");

fullName.Subscribe(name => Console.WriteLine($"Full name: {name}"));

firstName.Value = "Jane"; // Output: Full name: Jane Doe

Async Signals

var asyncSignal = new AsyncSignal<string>(async () => 
{
    await Task.Delay(1000);
    return "Data loaded!";
});

// Access the value
await asyncSignal.GetValueAsync(); // Returns "Data loaded!" after 1 second

Resource Signals

// Create a resource with a fetcher function
var userResource = new ResourceSignal<User>(
    async (ct) => await LoadUserFromDatabase(userId, ct)
);

// Subscribe to state changes
userResource.Subscribe(state =>
{
    if (state.IsLoading) Console.WriteLine("Loading...");
    if (state.HasData) Console.WriteLine($"User: {state.Data.Name}");
    if (state.HasError) Console.WriteLine($"Error: {state.Error.Message}");
});

// Load the resource
await userResource.LoadAsync();

HTTP Resources

Direct Usage
// Setup
var httpClient = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };
var resource = new HttpResource(httpClient);

// Subscribe to reactive updates
resource.Subscribe(response =>
{
    if (response?.IsSuccess == true)
    {
        // Handle successful response
    }
});

// GET - Fetch data
var todos = await resource.GetAsync<List<TodoItem>>("todos");

// POST - Create new resource
var newTodo = new TodoItem { Title = "New Task" };
var created = await resource.PostAsync<TodoItem, TodoItem>("todos", newTodo);

// PUT - Full update
var updated = new TodoItem { Id = 1, Title = "Updated Task", Completed = true };
var result = await resource.PutAsync<TodoItem, TodoItem>("todos/1", updated);

// PATCH - Partial update
var patch = new { completed = true };
var patched = await resource.PatchAsync<object, TodoItem>("todos/1", patch);

// DELETE - Remove resource
var deleted = await resource.DeleteAsync("todos/1");

// Access reactive signals
resource.IsLoading.Subscribe(loading => Console.WriteLine($"Loading: {loading}"));
resource.Error.Subscribe(error => Console.WriteLine($"Error: {error?.Message}"));
resource.LastStatusCode.Subscribe(status => Console.WriteLine($"Status: {status}"));
// Program.cs - Setup
builder.Services.AddHttpClient();
builder.Services.AddFluentSignalsBlazor(options =>
{
    options.BaseUrl = "https://api.example.com/";
    options.Timeout = TimeSpan.FromSeconds(30);
    options.RetryOptions = new RetryOptions
    {
        MaxRetryAttempts = 3,
        InitialRetryDelay = 100,
        UseExponentialBackoff = true
    };
});

// In your Blazor component or service
@inject IHttpResourceFactory ResourceFactory

@code {
    private HttpResource _todosResource;
    
    protected override void OnInitialized()
    {
        // Create with default options from DI
        _todosResource = ResourceFactory.Create();
        
        // Or create with custom base URL
        _todosResource = ResourceFactory.CreateWithBaseUrl("https://api.other.com/");
        
        // Or create with custom options
        _todosResource = ResourceFactory.CreateWithOptions(options =>
        {
            options.Timeout = TimeSpan.FromSeconds(60);
            options.DefaultHeaders["Authorization"] = "Bearer token";
        });
        
        // Subscribe to changes
        _todosResource.Subscribe(response =>
        {
            if (response?.IsSuccess == true)
            {
                StateHasChanged();
            }
        });
    }
    
    // All REST operations work the same way
    async Task LoadTodos() => await _todosResource.GetAsync<List<TodoItem>>("todos");
    
    async Task CreateTodo(TodoItem todo) => 
        await _todosResource.PostAsync<TodoItem, TodoItem>("todos", todo);
    
    async Task UpdateTodo(TodoItem todo) => 
        await _todosResource.PutAsync<TodoItem, TodoItem>($"todos/{todo.Id}", todo);
    
    async Task PatchTodo(int id, object patch) => 
        await _todosResource.PatchAsync<object, TodoItem>($"todos/{id}", patch);
    
    async Task DeleteTodo(int id) => 
        await _todosResource.DeleteAsync($"todos/{id}");
}

Typed HTTP Resources

Create strongly-typed HTTP resource classes for better organization and reusability:

using FluentSignals.Options.HttpResource;

// Option 1: Direct instantiation with HttpClient
public class UserResource : TypedHttpResource
{
    public UserResource(HttpClient httpClient) 
        : base(httpClient, "/api/users") { }
    
    public HttpResourceRequest<User> GetById(int id) => 
        Get<User>($"{BaseUrl}/{id}");
    
    public HttpResourceRequest<IEnumerable<User>> GetAll() => 
        Get<IEnumerable<User>>(BaseUrl);
    
    public HttpResourceRequest<User> Create(User user) => 
        Post<User>(BaseUrl, user);
    
    public HttpResourceRequest<User> Update(int id, User user) => 
        Put<User>($"{BaseUrl}/{id}", user);
    
    public HttpResourceRequest Delete(int id) => 
        Delete($"{BaseUrl}/{id}");
}

// Direct usage
var httpClient = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };
var users = new UserResource(httpClient);

// Execute requests
var userResource = await users.GetById(123).ExecuteAsync();
userResource.Subscribe(response => 
{
    if (response?.IsSuccess == true)
    {
        Console.WriteLine($"User loaded: {response.Data.Name}");
    }
});

// Option 2: Using with dependency injection (recommended)
// Define with attribute for factory-based creation
[HttpResource("/api/users")]
public class UserResource : TypedHttpResource
{
    public UserResource() { } // Parameterless constructor for factory
    
    // Same methods as above...
}

// Register in DI
services.AddTypedHttpResource<UserResource>();

// Inject and use
public class MyService
{
    private readonly UserResource _users;
    
    public MyService(UserResource users)
    {
        _users = users;
    }
    
    public async Task<User> GetUserAsync(int id)
    {
        var resource = await _users.GetById(id).ExecuteAsync();
        return resource.Value?.Data;
    }
}

// Advanced: Custom typed methods with full control
public class AdvancedUserResource : TypedHttpResource
{
    public AdvancedUserResource(HttpClient httpClient) 
        : base(httpClient, "/api/v2") { }
    
    // Fully typed search method with custom query parameters
    public HttpResourceRequest<PagedResult<User>> Search(UserSearchCriteria criteria)
    {
        return Get<PagedResult<User>>($"{BaseUrl}/users/search")
            .WithQueryParam("name", criteria.Name)
            .WithQueryParam("email", criteria.Email)
            .WithQueryParam("page", criteria.Page.ToString())
            .WithQueryParam("pageSize", criteria.PageSize.ToString());
    }
    
    // Bulk operations with typed request/response
    public HttpResourceRequest<BulkUpdateResult> BulkUpdate(BulkUpdateRequest<User> request)
    {
        return Post<BulkUpdateRequest<User>, BulkUpdateResult>($"{BaseUrl}/users/bulk", request)
            .WithHeader("X-Bulk-Operation", "true")
            .ConfigureResource(r => r.OnSuccess(() => Console.WriteLine("Bulk update completed")));
    }
    
    // Custom HTTP method with full control
    public HttpResourceRequest<MergeResult> MergeUsers(int sourceId, int targetId, MergeOptions options)
    {
        return Request<MergeOptions, MergeResult>(
                new HttpMethod("MERGE"), 
                $"{BaseUrl}/users/{sourceId}/merge/{targetId}", 
                options)
            .WithHeader("X-Merge-Strategy", options.Strategy.ToString());
    }
    
    // Complex request using the builder pattern
    public HttpResourceRequest<ImportResult> Import(Stream csvData, ImportOptions options)
    {
        return BuildRequest<ImportResult>($"{BaseUrl}/users/import")
            .WithMethod(HttpMethod.Post)
            .WithBody(new { data = csvData, options })
            .WithHeader("Content-Type", "multipart/form-data")
            .WithHeader("X-Import-Mode", options.Mode.ToString())
            .WithQueryParam("validate", options.ValidateBeforeImport.ToString())
            .Build();
    }
}

Advanced TypedHttpResource Features

Interceptors for Cross-Cutting Concerns
// Create a resource with interceptors
public class SecureUserResource : TypedHttpResourceWithInterceptors
{
    public SecureUserResource(HttpClient httpClient, ITokenProvider tokenProvider, ILogger<SecureUserResource> logger)
        : base(httpClient, "/api/users")
    {
        // Add authentication
        AddInterceptor(new BearerTokenInterceptor(tokenProvider.GetTokenAsync));
        
        // Add logging
        AddInterceptor(new LoggingInterceptor(logger));
        
        // Add retry logic
        AddInterceptor(new RetryInterceptor(maxRetries: 3, delay: TimeSpan.FromSeconds(1)));
    }
}

// Custom interceptor
public class ApiKeyInterceptor : IHttpResourceInterceptor
{
    private readonly string _apiKey;
    
    public ApiKeyInterceptor(string apiKey) => _apiKey = apiKey;
    
    public Task<HttpRequestMessage> OnRequestAsync(HttpRequestMessage request)
    {
        request.Headers.Add("X-Api-Key", _apiKey);
        return Task.FromResult(request);
    }
    
    public Task<HttpResponseMessage> OnResponseAsync(HttpResponseMessage response) => 
        Task.FromResult(response);
    
    public Task OnExceptionAsync(HttpRequestMessage request, Exception exception) => 
        Task.CompletedTask;
}
Response Caching
// Use built-in memory cache
var cache = new MemoryResponseCache();

// Cache responses
var user = await userResource.GetById(123)
    .WithCache(cache, "user_123", TimeSpan.FromMinutes(5))
    .ExecuteAsync();

// Or with automatic cache key generation
var users = await userResource.GetAll()
    .WithCache(cache, TimeSpan.FromMinutes(10))
    .ExecuteAsync();
Pagination Support
// Simple pagination
var pagedUsers = await userResource.GetUsers()
    .WithPaging(page: 2, pageSize: 50, sortBy: "name", sortDescending: true)
    .ExecuteAsync();

// Advanced pagination with filters
var request = new PagedRequest<User>
{
    Page = 1,
    PageSize = 20,
    SortBy = "createdAt",
    SortDescending = true,
    Filters = new Dictionary<string, string>
    {
        ["department"] = "Engineering",
        ["active"] = "true"
    }
};

var result = await userResource.SearchUsers()
    .WithPaging(request)
    .ExecuteAsync();
Bulk Operations
public class BulkUserResource : TypedHttpResourceWithBulk
{
    // Import users with progress tracking
    public async Task<BulkResult<User>> ImportUsers(List<CreateUserDto> users)
    {
        return await ExecuteBulkAsync<CreateUserDto, User>(
            "/api/users/import",
            users,
            batchSize: 100,
            onProgress: progress =>
            {
                Console.WriteLine($"Progress: {progress.PercentComplete}% ({progress.ProcessedItems}/{progress.TotalItems})");
            });
    }
    
    // Parallel bulk operations for better performance
    public async Task<BulkResult<User>> ImportUsersParallel(List<CreateUserDto> users)
    {
        return await ExecuteBulkParallelAsync<CreateUserDto, User>(
            "/api/users/import",
            users,
            batchSize: 100,
            maxParallelism: 4);
    }
}
Fluent Extensions
// Combine multiple features
var result = await userResource
    .SearchUsers("john")
    .WithPaging(1, 20)                              // Add pagination
    .WithCache(cache, TimeSpan.FromMinutes(5))      // Cache results
    .WithBearerToken(token)                         // Add auth token
    .WithTimeout(TimeSpan.FromSeconds(30))          // Set timeout
    .WithRetry(3, TimeSpan.FromSeconds(1))          // Configure retry
    .WithCancellation(cancellationToken)            // Support cancellation
    .ExecuteAsync();
Testing with Mocks
// Create mock responses for testing
var mockUser = new User { Id = 1, Name = "Test User" };
var mockRequest = new MockHttpResourceRequest<User>(mockUser, HttpStatusCode.OK);

// Test error scenarios
var errorRequest = new MockHttpResourceRequest<User>(
    new HttpRequestException("Network error"));

// Add delay to simulate network latency
var slowRequest = new MockHttpResourceRequest<User>(
    mockUser, HttpStatusCode.OK, delay: TimeSpan.FromSeconds(2));

Advanced Features

Signal Bus (Pub/Sub)

// Publisher
services.AddScoped<ISignalPublisher>();

await signalPublisher.PublishAsync(new UserCreatedEvent { UserId = 123 });

// Consumer
services.AddScoped<ISignalConsumer<UserCreatedEvent>>();

signalConsumer.Subscribe(message => 
{
    Console.WriteLine($"User created: {message.UserId}");
});

Queue-based Subscriptions

// Subscribe with message queue - receives all messages, even those published before subscription
subscription = signalConsumer.SubscribeByQueue(message =>
{
    ProcessMessage(message);
}, processExistingMessages: true);

Integration with Blazor

FluentSignals integrates seamlessly with Blazor applications. See the FluentSignals.Blazor package for Blazor-specific features and components.

Documentation

For more detailed documentation, examples, and API reference, visit our GitHub repository.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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 (4)

Showing the top 4 NuGet packages that depend on FluentSignals:

Package Downloads
FluentSignals.Blazor

Blazor integration for FluentSignals - A reactive state management library. Includes SignalBus for component communication, HTTP resource components, typed resource factories, and Blazor-specific helpers.

FluentSignals.SignalBus

Event bus and messaging patterns for FluentSignals including publish/subscribe, message queuing, and advanced patterns like batching and multi-tenancy.

FluentSignals.SignalR

SignalR integration for FluentSignals providing real-time reactive resources with automatic reconnection and state management.

FluentSignals.Http

HTTP client extensions for FluentSignals including reactive HTTP resources, typed API clients, interceptors, caching, and advanced features for building robust HTTP integrations.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.5 22 7/17/2025
2.1.4 48 7/17/2025
2.1.3 58 7/15/2025
2.1.2 153 7/9/2025
2.1.1 157 7/8/2025
2.1.0 150 7/8/2025
2.0.0 146 6/29/2025
1.1.3 146 6/19/2025
1.1.2 146 6/17/2025
1.1.1 142 6/16/2025
1.1.0 143 6/16/2025
1.0.0 133 6/15/2025

v1.1.3: Added TypedHttpResource for strongly-typed API clients, factory pattern with DI support, fluent request builders, and custom HTTP method support. See https://github.com/andres-m-rodriguez/FluentSignals/blob/main/CHANGELOG.md for details.