SweetMock.Extensions.Options 0.9.41

dotnet add package SweetMock.Extensions.Options --version 0.9.41
                    
NuGet\Install-Package SweetMock.Extensions.Options -Version 0.9.41
                    
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="SweetMock.Extensions.Options" Version="0.9.41" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SweetMock.Extensions.Options" Version="0.9.41" />
                    
Directory.Packages.props
<PackageReference Include="SweetMock.Extensions.Options" />
                    
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 SweetMock.Extensions.Options --version 0.9.41
                    
#r "nuget: SweetMock.Extensions.Options, 0.9.41"
                    
#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 SweetMock.Extensions.Options@0.9.41
                    
#: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=SweetMock.Extensions.Options&version=0.9.41
                    
Install as a Cake Addin
#tool nuget:?package=SweetMock.Extensions.Options&version=0.9.41
                    
Install as a Cake Tool

SweetMock.Extensions.Options

A comprehensive extension library for mocking IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> in .NET tests using the SweetMock framework.

Features

  • Mock IOptions<T>: Simple, immutable configuration mocking
  • Mock IOptionsSnapshot<T>: Scoped configuration with named options support
  • Mock IOptionsMonitor<T>: Dynamic configuration with change notifications
  • Auto-initialization: Automatically creates default option values when possible
  • Named Options: Support for multiple named configuration instances
  • Modify Configuration: Update option values during tests
  • Change Notifications: Test configuration change callbacks with IOptionsMonitor

Installation

dotnet add package SweetMock.Extensions.Options

Quick Start

Basic IOptions<T> Mock

using Microsoft.Extensions.Options;

[Fixture<MyService>]
[Mock<IOptions<AppSettings>, MockIOptions<AppSettings>>]
public class MyServiceTests
{
    [Fact]
    public void Service_UsesConfiguredOptions()
    {
        // Arrange
        var settings = new AppSettings { ConnectionString = "Server=localhost" };
        var fixture = Fixture.MyService(config =>
            config.appSettings.Value(() => settings)
        );
        var sut = fixture.CreateMyService();

        // Act
        var result = sut.Connect();

        // Assert
        Assert.True(result);
    }
}

public class MyService(IOptions<AppSettings> appSettings)
{
    public bool Connect() => appSettings.Value.ConnectionString != null;
}

public class AppSettings
{
    public string? ConnectionString { get; set; }
}

IOptions<T>

IOptions<T> provides simple, immutable configuration values. Best for configuration that doesn't change during the application lifetime.

Auto-initialization

If your options class has a parameterless constructor, it will be auto-initialized:

[Fact]
public void DefaultOptions_AreAutoCreated()
{
    // Arrange
    var fixture = Fixture.MyService();
    var sut = fixture.CreateMyService();

    // Act
    var settings = sut.GetSettings();

    // Assert - Uses default values from AppSettings constructor
    Assert.Equal("DefaultValue", settings.Name);
}

public class AppSettings
{
    public string Name { get; set; } = "DefaultValue";
    public int Timeout { get; set; } = 30;
}

Setting Option Values

[Fact]
public void CustomOptions_AreUsed()
{
    // Arrange
    var customSettings = new AppSettings
    {
        Name = "CustomName",
        Timeout = 60
    };

    var fixture = Fixture.MyService(config =>
        config.settings.Value(() => customSettings)
    );
    var sut = fixture.CreateMyService();

    // Act & Assert
    Assert.Equal("CustomName", sut.GetSettings().Name);
    Assert.Equal(60, sut.GetSettings().Timeout);
}

Modifying Options

[Fact]
public void Options_CanBeModified()
{
    // Arrange
    var fixture = Fixture.MyService();
    var sut = fixture.CreateMyService();

    // Act - Modify after initialization
    fixture.Config.settings.Modify(s => s.Name = "Modified");

    // Assert
    Assert.Equal("Modified", sut.GetSettings().Name);
}

IOptionsSnapshot<T>

IOptionsSnapshot<T> provides scoped configuration values that can be reloaded. Supports named options for different configurations.

Default and Named Options

[Fixture<ConfigService>]
[Mock<IOptionsSnapshot<DatabaseSettings>, MockIOptionsSnapshot<DatabaseSettings>>]
public class ConfigServiceTests
{
    [Fact]
    public void NamedOptions_ReturnDifferentValues()
    {
        // Arrange
        var fixture = Fixture.ConfigService(config =>
        {
            config.dbSettings.Get("Primary", new DatabaseSettings
            {
                ConnectionString = "Server=primary"
            });
            config.dbSettings.Get("Secondary", new DatabaseSettings
            {
                ConnectionString = "Server=secondary"
            });
        });
        var sut = fixture.CreateConfigService();

        // Act
        var primary = sut.GetConnection("Primary");
        var secondary = sut.GetConnection("Secondary");

        // Assert
        Assert.Equal("Server=primary", primary);
        Assert.Equal("Server=secondary", secondary);
    }
}

public class ConfigService(IOptionsSnapshot<DatabaseSettings> dbSettings)
{
    public string GetConnection(string name) => dbSettings.Get(name).ConnectionString;
}

public class DatabaseSettings
{
    public string ConnectionString { get; set; } = "default";
}

Initialize and Modify

[Fact]
public void Snapshot_CanBeInitializedAndModified()
{
    // Arrange
    var fixture = Fixture.ConfigService(config =>
        config.dbSettings.Initialize(new DatabaseSettings
        {
            ConnectionString = "Initial"
        })
    );
    var sut = fixture.CreateConfigService();

    // Verify initial value
    Assert.Equal("Initial", sut.GetDefaultConnection());

    // Act - Modify during test
    fixture.Config.dbSettings.Modify(s => s.ConnectionString = "Updated");

    // Assert
    Assert.Equal("Updated", sut.GetDefaultConnection());
}

Multiple Named Configurations

[Fact]
public void MultipleNamedOptions_AreIndependent()
{
    // Arrange
    var fixture = Fixture.ConfigService(config =>
    {
        config.dbSettings.Initialize("Config1", new DatabaseSettings());
        config.dbSettings.Initialize("Config2", new DatabaseSettings());
    });
    var sut = fixture.CreateConfigService();

    // Act - Modify only one
    fixture.Config.dbSettings.Modify("Config1", s => s.ConnectionString = "Modified");

    // Assert - Only Config1 changed
    Assert.Equal("Modified", sut.GetConnection("Config1"));
    Assert.Equal("default", sut.GetConnection("Config2"));
}

IOptionsMonitor<T>

IOptionsMonitor<T> provides dynamic configuration with change notifications. Use this when you need to test configuration reloading scenarios.

Basic Monitor Usage

[Fixture<MonitoredService>]
[Mock<IOptionsMonitor<AppConfig>, MockIOptionsMonitor<AppConfig>>]
public class MonitoredServiceTests
{
    [Fact]
    public void Monitor_TracksConfigChanges()
    {
        // Arrange
        var fixture = Fixture.MonitoredService(config =>
            config.appConfig.Initialize(new AppConfig { Feature = "Initial" })
        );
        var sut = fixture.CreateMonitoredService();

        // Act
        var before = sut.CurrentFeature;
        fixture.Config.appConfig.Modify(c => c.Feature = "Updated");
        var after = sut.CurrentFeature;

        // Assert
        Assert.Equal("Initial", before);
        Assert.Equal("Updated", after);
    }
}

public class MonitoredService(IOptionsMonitor<AppConfig> appConfig)
{
    public string CurrentFeature => appConfig.CurrentValue.Feature;
}

public class AppConfig
{
    public string Feature { get; set; } = "Default";
}

Change Notifications

[Fact]
public void OnChange_NotifiesSubscribers()
{
    // Arrange
    var changes = new List<string>();
    var monitor = Mock.IOptionsMonitor<AppConfig>(out var config);

    // Subscribe to changes
    monitor.OnChange(settings => changes.Add(settings.Feature));

    // Act
    config.Modify(s => s.Feature = "Change1");
    config.Modify(s => s.Feature = "Change2");
    config.Modify(s => s.Feature = "Change3");

    // Assert
    Assert.Equal(3, changes.Count);
    Assert.Equal("Change1", changes[0]);
    Assert.Equal("Change2", changes[1]);
    Assert.Equal("Change3", changes[2]);
}

Disposing Change Listeners

[Fact]
public void DisposedListener_StopsReceivingNotifications()
{
    // Arrange
    var changes = new List<string>();
    var monitor = Mock.IOptionsMonitor<AppConfig>(out var config);

    var listener = monitor.OnChange(settings => changes.Add(settings.Feature));

    // Act
    config.Modify(s => s.Feature = "Change1");
    config.Modify(s => s.Feature = "Change2");
    listener.Dispose(); // Stop listening
    config.Modify(s => s.Feature = "Change3");

    // Assert
    Assert.Equal(2, changes.Count); // Only first 2 changes captured
    Assert.DoesNotContain("Change3", changes);
}

Named Monitors

[Fact]
public void NamedMonitors_TrackIndependently()
{
    // Arrange
    var fixture = Fixture.MonitoredService(config =>
    {
        config.appConfig.Initialize("Service1", new AppConfig { Feature = "S1" });
        config.appConfig.Initialize("Service2", new AppConfig { Feature = "S2" });
    });
    var sut = fixture.CreateMonitoredService();

    // Act
    fixture.Config.appConfig.Modify("Service1", c => c.Feature = "S1-Modified");
    var service1 = sut.GetConfig("Service1");
    var service2 = sut.GetConfig("Service2");

    // Assert
    Assert.Equal("S1-Modified", service1);
    Assert.Equal("S2", service2); // Unchanged
}

Complete Example

[Fixture<EmailService>]
[Mock<IOptionsMonitor<EmailSettings>, MockIOptionsMonitor<EmailSettings>>]
public class EmailServiceTests
{
    [Fact]
    public void EmailService_ReactsToConfigChanges()
    {
        // Arrange
        var sentEmails = new List<string>();
        var fixture = Fixture.EmailService(config =>
            config.emailSettings.Initialize(new EmailSettings
            {
                SmtpServer = "smtp.example.com",
                Port = 587,
                Enabled = true
            })
        );
        var sut = fixture.CreateEmailService();

        // Act - Send with initial config
        sut.SendEmail("test@example.com", "Hello");

        // Disable emails via config change
        fixture.Config.emailSettings.Modify(s => s.Enabled = false);

        // Try to send again
        sut.SendEmail("test2@example.com", "World");

        // Assert
        Assert.Single(sentEmails); // Only first email sent
    }
}

public class EmailService
{
    private readonly IOptionsMonitor<EmailSettings> _settings;
    private readonly IDisposable _changeListener;

    public EmailService(IOptionsMonitor<EmailSettings> settings)
    {
        _settings = settings;
        _changeListener = settings.OnChange(newSettings =>
        {
            Console.WriteLine($"Config changed: Enabled={newSettings.Enabled}");
        });
    }

    public void SendEmail(string to, string message)
    {
        if (!_settings.CurrentValue.Enabled)
        {
            Console.WriteLine("Email disabled");
            return;
        }

        // Send email logic...
    }
}

public class EmailSettings
{
    public string SmtpServer { get; set; } = "localhost";
    public int Port { get; set; } = 25;
    public bool Enabled { get; set; } = true;
}

API Reference

MockIOptions<TOptions>

Provides immutable configuration values.

// Auto-initialized if TOptions has parameterless constructor
var options = Mock.IOptions<AppSettings>();

// Set value explicitly
var options = Mock.IOptions<AppSettings>(config =>
    config.Value(() => new AppSettings { Name = "Test" })
);

// Modify after creation
config.Modify(settings => settings.Name = "Updated");

MockIOptionsSnapshot<TOptions>

Provides scoped configuration with named options.

// Default option
config.snapshot.Initialize(new AppSettings());

// Named options
config.snapshot.Get("Config1", new AppSettings());
config.snapshot.Get("Config2", new AppSettings());

// Modify default
config.snapshot.Modify(s => s.Name = "New");

// Modify named
config.snapshot.Modify("Config1", s => s.Name = "New");

MockIOptionsMonitor<TOptions>

Provides dynamic configuration with change notifications.

// Initialize
config.monitor.Initialize(new AppSettings());
config.monitor.Initialize("Named", new AppSettings());

// Modify (triggers OnChange callbacks)
config.monitor.Modify(s => s.Name = "Updated");
config.monitor.Modify("Named", s => s.Name = "Updated");

// Subscribe to changes
var listener = monitor.OnChange((settings, name) =>
{
    Console.WriteLine($"Config '{name}' changed");
});

// Unsubscribe
listener.Dispose();

Error Handling

Options That Cannot Be Auto-Created

If your options class doesn't have a parameterless constructor, you must initialize it explicitly:

// This will throw NotExplicitlyMockedException
public class RequiredParamOptions(string required);

[Fact]
public void NonAutoCreatable_ThrowsException()
{
    var sut = Mock.IOptions<RequiredParamOptions>();

    // Throws NotExplicitlyMockedException
    Assert.Throws<NotExplicitlyMockedException>(() => sut.Value);
}

// Solution: Initialize explicitly
var fixture = Fixture.MyService(config =>
    config.options.Value(() => new RequiredParamOptions("value"))
);

Unknown Named Options

Accessing unknown named options throws ArgumentException:

[Fact]
public void UnknownOption_ThrowsArgumentException()
{
    var snapshot = Mock.IOptionsSnapshot<AppSettings>();

    Assert.Throws<ArgumentException>(() => snapshot.Get("Unknown"));
}

Choosing the Right Interface

  • IOptions<T>: Use for configuration that never changes during application lifetime (e.g., static app settings)
  • IOptionsSnapshot<T>: Use for scoped configuration that can be reloaded per request/scope (e.g., per-request settings)
  • IOptionsMonitor<T>: Use for singleton services that need to react to configuration changes in real-time (e.g., feature flags, dynamic settings)

Requirements

  • .NET 8.0, 9.0, or 10.0
  • SweetMock
  • Microsoft.Extensions.Options

License

See the main SweetMock repository for license information.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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 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.9.41 130 5/22/2026
0.9.40 117 5/19/2026
0.9.39 130 4/29/2026
0.9.38 116 4/21/2026