Noundry.Tuxedo.Cufflink.CLI 0.4.0

dotnet tool install --global Noundry.Tuxedo.Cufflink.CLI --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Noundry.Tuxedo.Cufflink.CLI --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Noundry.Tuxedo.Cufflink.CLI&version=0.4.0
                    
nuke :add-package Noundry.Tuxedo.Cufflink.CLI --version 0.4.0
                    

Cufflink is a fake data generation library and CLI tool for Tuxedo ORM. It uses Bogus to generate realistic test data based on your entity models, automatically honoring foreign key relationships and primary key constraints.

Features

  • ๐ŸŽฒ Intelligent Fake Data Generation: Uses Bogus to generate realistic data based on property names and types
  • ๐Ÿ”— FK/PK Relationship Awareness: Automatically detects and honors foreign key relationships
  • ๐Ÿ“Š Bulk Insert Performance: Uses Tuxedo's bulk operations for efficient data loading
  • ๐Ÿ—„๏ธ Multi-Database Support: Works with SQL Server, PostgreSQL, MySQL, and SQLite
  • ๐Ÿ“ JSON/JSONB Column Support: Generate fake JSON data using JSON schemas or auto-generation
  • ๐Ÿ› ๏ธ Three Usage Modes:
    • Wizard Mode: Interactive CLI for easy configuration (recommended for beginners)
    • Command Mode: Direct CLI commands for automation
    • Library: Use programmatically in your Program.cs or test setup
  • ๐ŸŽฏ Topological Sort: Automatically determines correct insert order based on dependencies
  • ๐Ÿ–ฅ๏ธ Spectre.Console UI: Beautiful CLI interface with progress indication

Installation

As a Library

dotnet add package Noundry.Tuxedo.Cufflink

As a Global Tool

dotnet tool install --global Noundry.Tuxedo.Cufflink.CLI

Quick Start

CLI Usage

# Launch interactive wizard - easiest way to get started!
cufflink wizard

# The wizard will guide you through:
# 1. Discovering your entity models
# 2. Selecting which models to generate data for
# 3. Configuring JSON/JSONB columns (if any)
# 4. Setting record counts and database options
Command Mode
# Generate 1000 fake records per table
cufflink generate --records 1000

# Specify database provider and connection string
cufflink generate --records 500 --provider SqlServer --connection-string "Server=.;Database=TestDb;..."

# Generate with JSON schema support
cufflink generate --records 1000 --json-schema-dir ./schemas

# Generate for a specific directory
cufflink generate --records 100 --directory ./bin/Debug/net9.0

# Show information about discovered models
cufflink info

Library Usage

Example 1: Generate Fake Data
using Noundry.Tuxedo.Cufflink;
using Noundry.Tuxedo.Cufflink.Core;
using Noundry.Tuxedo.Contrib;

// Define your models using Tuxedo attributes
[Table("Categories")]
public class Category
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

[Table("Products")]
public class Product
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // โœ… Automatically detected as FK to Categories.Id (by naming convention)
    // Supports int, long, and Guid FK types
    public int CategoryId { get; set; }
}

// Generate fake data
var engine = new CufflinkEngine();
await engine.GenerateFakeAsync(
    assembly: typeof(Program).Assembly,
    recordCount: 1000,
    connectionString: "Data Source=app.db",
    provider: DatabaseProvider.SQLite);
Example 2: Seed Predefined Data
var seedData = new Dictionary<Type, List<object>>
{
    {
        typeof(Category),
        new List<object>
        {
            new Category { Id = 1, Name = "Electronics" },
            new Category { Id = 2, Name = "Books" }
        }
    },
    {
        typeof(Product),
        new List<object>
        {
            new Product { Id = 1, Name = "Laptop", Price = 999.99m, CategoryId = 1 },
            new Product { Id = 2, Name = "Novel", Price = 14.99m, CategoryId = 2 }
        }
    }
};

await engine.SeedAsync(
    seedData: seedData,
    connectionString: "Data Source=app.db",
    provider: DatabaseProvider.SQLite);
Example 3: Use in Program.cs for Testing
var builder = WebApplication.CreateBuilder(args);

// ... configure services ...

var app = builder.Build();

// Generate test data in development environment
if (app.Environment.IsDevelopment())
{
    var engine = new CufflinkEngine();
    await engine.GenerateFakeAsync(
        assembly: typeof(Program).Assembly,
        recordCount: 100,
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        provider: DatabaseProvider.SQLite);
}

app.Run();
Example 4: Generate for Specific Model Types
using Noundry.Tuxedo.Cufflink;
using Noundry.Tuxedo.Cufflink.Core;

var engine = new CufflinkEngine();

// Generate fake data for only the specified model types
var modelTypes = new[] { typeof(Category), typeof(Product) };
await engine.GenerateFakeAsync(
    modelTypes: modelTypes,
    recordCount: 500,
    connectionString: "Data Source=app.db",
    provider: DatabaseProvider.SQLite);

This overload is useful when you want to generate data for a subset of your models rather than scanning an entire assembly or directory.

How It Works

1. Model Discovery

Cufflink scans your assemblies for classes decorated with [Table] attributes from Tuxedo:

using Noundry.Tuxedo.Contrib;

[Table("Users")]
public class User
{
    [Key]
    public int Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
}

The ModelScanner class also exposes two convenience methods for advanced scenarios:

  • ModelScanner.ScanType(Type) -- Scans a single type and returns an EntityModel if it has a [Table] attribute, or null otherwise. Used internally by the GenerateFakeAsync(IEnumerable<Type>, ...) overload.
  • ModelScanner.ScanCurrentDirectory() -- Scans all DLL assemblies in the current working directory for entity models. Equivalent to calling ScanDirectory(Directory.GetCurrentDirectory()).

2. Relationship Analysis

Cufflink automatically detects foreign key relationships using naming conventions - no attributes required!

  • Primary Method: Properties ending in "Id" (e.g., CategoryId โ†’ Category)
  • Supported types: int, long, and Guid FK columns are detected by convention
  • Optional: [ForeignKey("TableName")] attribute (requires Bowtie package)

Note: Nullable FK columns (int?, long?, Guid?) are not detected by convention. If you need nullable FK relationships, use the [ForeignKey("TableName")] attribute from Bowtie.

[Table("Products")]
public class Product
{
    [Key]
    public int Id { get; set; }

    // โœ… Automatically detected - CategoryId (int) references Category.Id
    public int CategoryId { get; set; }

    // โœ… Automatically detected - ManufacturerId (long) references Manufacturer.Id
    public long ManufacturerId { get; set; }

    // โœ… Automatically detected - CreatedById (Guid) references CreatedBy.Id
    public Guid CreatedById { get; set; }

    // โŒ NOT detected - nullable types require [ForeignKey] attribute
    public int? OptionalCategoryId { get; set; }
}

// Optional: If you have Bowtie installed, you can use explicit attributes:
// [ForeignKey("Categories")]
// public int CategoryId { get; set; }

3. Topological Sort

Cufflink performs a topological sort to determine the correct insertion order based on FK dependencies:

Categories (no dependencies)
  โ†“
Products (depends on Categories)
  โ†“
Orders (depends on Products)

4. Intelligent Data Generation

Bogus generates realistic data based on property names and types:

public class User
{
    public string Email { get; set; }      // โ†’ "john.doe@example.com"
    public string FirstName { get; set; }  // โ†’ "John"
    public string LastName { get; set; }   // โ†’ "Doe"
    public string Phone { get; set; }      // โ†’ "(555) 123-4567"
    public string Address { get; set; }    // โ†’ "123 Main Street, City, ST 12345"
    public decimal Price { get; set; }     // โ†’ Random decimal
    public int Age { get; set; }           // โ†’ Random int (18-80 for age)
    public DateTime CreatedAt { get; set; }// โ†’ Recent past date
}

5. Bulk Insert

Uses Tuxedo's high-performance bulk operations to insert data efficiently:

// Inserts 10,000 records in batches of 1000
await engine.GenerateFakeAsync(
    recordCount: 10000,
    batchSize: 1000,
    ...);

JSON/JSONB Column Support

Cufflink provides comprehensive support for generating fake data for JSON and JSONB columns in PostgreSQL, MySQL, SQL Server, and SQLite.

Three Ways to Generate JSON Data

1. Auto-Generation (Easiest)

Cufflink automatically detects properties that are likely JSON columns based on naming conventions:

[Table("Products")]
public class Product
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    // โœ… Auto-detected as JSON column (ends with "Metadata")
    public string? Metadata { get; set; }

    // โœ… Also auto-detected: *Data, *Json, *Config, *Settings, *Attributes, *Properties
    // โœ… Contains-based detection: *jsonb* (e.g., JsonbPayload, UserJsonbData)
    public string? ConfigData { get; set; }
}

Cufflink will generate simple JSON objects based on the property name:

// Product.Metadata will contain:
{
  "key": "some-value",
  "value": "another-value"
}

Use JSON Schema files for full control over the structure and data types:

using Noundry.Tuxedo.Cufflink.Schema;

[Table("Products")]
public class Product
{
    [Key]
    public int Id { get; set; }

    // ๐Ÿ“ Specify JSON schema file (relative to project or schemas/ directory)
    [JsonSchema("product-metadata.json")]
    public string? Metadata { get; set; }
}

schemas/product-metadata.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "sku": {
      "type": "string",
      "faker": "random.alphaNumeric"
    },
    "weight": {
      "type": "number",
      "minimum": 0.1,
      "maximum": 100
    },
    "dimensions": {
      "type": "object",
      "properties": {
        "length": { "type": "number", "minimum": 1, "maximum": 100 },
        "width": { "type": "number", "minimum": 1, "maximum": 100 },
        "height": { "type": "number", "minimum": 1, "maximum": 100 }
      }
    },
    "tags": {
      "type": "array",
      "minItems": 1,
      "maxItems": 5,
      "items": {
        "type": "string",
        "faker": "commerce.productAdjective"
      }
    },
    "inStock": {
      "type": "boolean"
    }
  }
}

Generated JSON output:

{
  "sku": "A7X9K2",
  "weight": 45.7,
  "dimensions": {
    "length": 23.4,
    "width": 12.8,
    "height": 8.5
  },
  "tags": ["premium", "durable", "lightweight"],
  "inStock": true
}
3. Inline Schemas

For simple cases, use inline JSON schemas:

[Table("Users")]
public class User
{
    [Key]
    public int Id { get; set; }

    [JsonSchema(InlineSchema = @"{
        ""type"": ""object"",
        ""properties"": {
            ""theme"": { ""type"": ""string"", ""enum"": [""light"", ""dark""] },
            ""notifications"": { ""type"": ""boolean"" }
        }
    }")]
    public string? Settings { get; set; }
}

Bogus Integration in JSON Schemas

Use the faker extension property to leverage Bogus formatters:

{
  "type": "object",
  "properties": {
    "email": {
      "type": "string",
      "faker": "internet.email"
    },
    "fullName": {
      "type": "string",
      "faker": "name.fullName"
    },
    "company": {
      "type": "string",
      "faker": "company.companyName"
    },
    "city": {
      "type": "string",
      "faker": "address.city"
    }
  }
}

Supported Bogus formatters:

  • name.firstName, name.lastName, name.fullName, name.jobTitle
  • internet.email, internet.username, internet.url, internet.domainName, internet.ip
  • address.street, address.city, address.state, address.zipCode, address.country
  • phone.phoneNumber
  • company.companyName
  • commerce.product, commerce.color, commerce.department, commerce.productAdjective
  • random.word, random.words, random.sentence, random.alphaNumeric

JSON Schema Format Support

Cufflink recognizes standard JSON Schema formats:

{
  "email": { "type": "string", "format": "email" },
  "website": { "type": "string", "format": "uri" },
  "created": { "type": "string", "format": "date-time" },
  "ipAddress": { "type": "string", "format": "ipv4" },
  "id": { "type": "string", "format": "uuid" }
}

CLI Usage with JSON Schemas

Wizard Mode
cufflink wizard

# The wizard will:
# 1. Detect JSON columns automatically
# 2. Ask how you want to generate JSON data
# 3. Allow you to specify a schema directory
Command Mode
# Specify directory containing JSON schema files
cufflink generate --records 1000 --json-schema-dir ./schemas

# Schemas are matched by filename:
# - Metadata property โ†’ metadata.json or product-metadata.json
# - ConfigData property โ†’ config-data.json or configdata.json

Database-Specific Notes

Database Column Type Notes
PostgreSQL JSONB, JSON Native JSON support, use JSONB for better performance
MySQL JSON Native JSON type (MySQL 5.7+)
SQL Server NVARCHAR(MAX) Store as text, use ISJSON() and JSON_VALUE() functions
SQLite TEXT Store as text, use json() functions

Complete Example

using Noundry.Tuxedo.Contrib;
using Noundry.Tuxedo.Cufflink.Schema;

[Table("Products")]
public class Product
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    public int CategoryId { get; set; }  // FK to Categories

    // JSON column with schema
    [JsonSchema("product-metadata.json")]
    public string? Metadata { get; set; }

    // Auto-detected JSON column
    public string? CustomAttributes { get; set; }
}

// Generate data
var engine = new CufflinkEngine();
await engine.GenerateFakeAsync(
    assembly: typeof(Product).Assembly,
    recordCount: 1000,
    connectionString: "Host=localhost;Database=mydb;...",
    provider: DatabaseProvider.PostgreSQL);

// Result: Products table with realistic JSON in Metadata and CustomAttributes columns

CLI Commands

wizard

Interactive wizard for generating fake data (recommended for beginners).

cufflink wizard [options]

Options:

  • -d|--directory <path>: Directory to scan for models (default: current directory)

Features:

  • Step-by-step guided setup
  • Automatic JSON column detection
  • Visual model selection
  • Connection string auto-detection
  • Configuration summary before execution

generate

Generate fake data for all discovered models (command mode).

cufflink generate [options]

Options:

  • -r|--records <number>: Number of records per table (default: 100)
  • -c|--connection-string <string>: Database connection string (auto-detected from appsettings.json if omitted)
  • -p|--provider <SqlServer|PostgreSQL|MySQL|SQLite>: Database provider (default: SQLite)
  • -d|--directory <path>: Directory to scan for models (default: current directory)
  • -b|--batch-size <number>: Batch size for bulk inserts (default: 1000)
  • --json-schema-dir <path>: Directory containing JSON schema files for JSON/JSONB columns

Examples:

# Generate 1000 records with auto-detected connection string
cufflink generate --records 1000

# SQL Server with explicit connection
cufflink generate -r 500 -p SqlServer -c "Server=.;Database=TestDb;Integrated Security=true"

# PostgreSQL
cufflink generate -r 200 -p PostgreSQL -c "Host=localhost;Database=testdb;Username=postgres;Password=pass"

# MySQL with custom directory
cufflink generate -r 100 -p MySQL -d ./bin/Release/net9.0

info

Display information about discovered models and their relationships.

cufflink info [options]

Options:

  • -d|--directory <path>: Directory to scan (default: current directory)

Example Output:

The banner is rendered using Spectre.Console's FigletText and will display a stylized "Cufflink" heading in blue in terminals that support it.

[Cufflink banner rendered via Spectre.Console FigletText]

Model Information

Found 3 entity models:

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ Table     โ”‚ Type     โ”‚ Properties โ”‚ Foreign Keys  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Categoriesโ”‚ Category โ”‚ 3          โ”‚ 0             โ”‚
โ”‚ Products  โ”‚ Product  โ”‚ 6          โ”‚ 1             โ”‚
โ”‚ Orders    โ”‚ Order    โ”‚ 4          โ”‚ 1             โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Insert Order (respecting FK dependencies):
  1. Categories
  2. Products
  3. Orders

Supported Databases

Cufflink supports all databases that Tuxedo supports:

Provider Connection String Example
SQL Server Server=.;Database=TestDb;Integrated Security=true;TrustServerCertificate=true
PostgreSQL Host=localhost;Database=testdb;Username=postgres;Password=pass
MySQL Server=localhost;Database=testdb;Uid=root;Pwd=pass;
SQLite Data Source=test.db

Configuration

appsettings.json

Cufflink can automatically detect connection strings from appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=app.db",
    "SqlServer": "Server=.;Database=TestDb;...",
    "PostgreSQL": "Host=localhost;Database=testdb;..."
  }
}

Cufflink will search for connection strings in this order:

  1. Direct connection string (if provided via -c flag or connectionString parameter)
  2. DefaultConnection
  3. Database, DbConnection, SqlConnection, etc.
  4. First available connection string

Programmatic JSON Schema Directory

When using Cufflink as a library, you can set the JSON schema base directory programmatically using SetJsonSchemaDirectory. Schema file paths (from [JsonSchema("file.json")] attributes) will be resolved relative to this directory.

var engine = new CufflinkEngine();
engine.SetJsonSchemaDirectory("/path/to/schemas");

await engine.GenerateFakeAsync(
    assembly: typeof(Product).Assembly,
    recordCount: 1000,
    connectionString: "Data Source=app.db",
    provider: DatabaseProvider.SQLite);

IServiceProvider Extension Method

Cufflink provides a GenerateFakeDataAsync extension method on IServiceProvider for convenient integration with ASP.NET Core applications. This method creates a CufflinkEngine internally and generates fake data using models discovered in the current directory.

using Noundry.Tuxedo.Cufflink;

var builder = WebApplication.CreateBuilder(args);
// ... configure services ...

var app = builder.Build();

// ASP.NET Core integration
if (app.Environment.IsDevelopment())
{
    await app.Services.GenerateFakeDataAsync(
        recordCount: 100,
        connectionString: "Data Source=app.db",
        provider: DatabaseProvider.SQLite);
}

app.Run();

Parameters:

  • recordCount (int): Number of records to generate per table
  • connectionString (string?, optional): Database connection string. If omitted, auto-detected from appsettings.json
  • provider (DatabaseProvider, optional): Database provider (default: SQLite)

Advanced Usage

Custom Seed Data Providers

public class CustomSeeder
{
    public static Dictionary<Type, List<object>> GetSeedData()
    {
        return new Dictionary<Type, List<object>>
        {
            { typeof(Category), GetCategories() },
            { typeof(Product), GetProducts() }
        };
    }

    private static List<object> GetCategories()
    {
        return new List<object>
        {
            new Category { Id = 1, Name = "Electronics" },
            new Category { Id = 2, Name = "Books" }
        };
    }

    private static List<object> GetProducts()
    {
        return new List<object>
        {
            new Product { Id = 1, Name = "Laptop", CategoryId = 1 },
            new Product { Id = 2, Name = "Novel", CategoryId = 2 }
        };
    }
}

// Use in seeding
var engine = new CufflinkEngine();
await engine.SeedAsync(
    CustomSeeder.GetSeedData(),
    connectionString,
    DatabaseProvider.SQLite);

Integration with xUnit/NUnit Tests

[TestFixture]
public class ProductTests
{
    private CufflinkEngine _cufflink = null!;
    private string _connectionString = null!;

    [SetUp]
    public async Task Setup()
    {
        _connectionString = "Data Source=:memory:";
        _cufflink = new CufflinkEngine();

        // Create schema (using Bowtie or manual SQL)
        // ...

        // Generate test data
        await _cufflink.GenerateFakeAsync(
            assembly: typeof(Product).Assembly,
            recordCount: 50,
            connectionString: _connectionString,
            provider: DatabaseProvider.SQLite);
    }

    [Test]
    public async Task GetProducts_ReturnsData()
    {
        using var connection = new SqliteConnection(_connectionString);
        var products = await connection.GetAllAsync<Product>();

        Assert.That(products.Count(), Is.EqualTo(50));
    }
}

Performance

Cufflink uses Tuxedo's bulk operations for high-performance data insertion:

Records Traditional Insert Bulk Insert Speedup
100 ~500ms ~50ms 10x
1,000 ~5s ~200ms 25x
10,000 ~50s ~1s 50x
100,000 ~500s ~8s 62x

Tips for best performance:

  • Use appropriate batch sizes (500-5000 depending on record size)
  • Disable indexes before bulk insert (re-enable after)
  • Use transactions
  • Consider using SQLite for test data (fastest)

Comparison with Other Tools

Feature Cufflink EF Core Seed Data Bogus Alone
Automatic FK Resolution โœ… โŒ โŒ
Multi-Database Support โœ… โœ… โŒ
Bulk Insert Performance โœ… โŒ โŒ
JSON/JSONB Support โœ… โŒ โŒ
Interactive Wizard โœ… โŒ โŒ
CLI Tool โœ… โŒ โŒ
Library Usage โœ… โœ… โœ…
Intelligent Data Gen โœ… โŒ โœ…

Requirements

  • .NET 8.0, 9.0, or 10.0
  • Noundry.Tuxedo (automatically installed)
  • Bogus 35.6+ (automatically installed)
  • NJsonSchema 11.0+ (automatically installed - for JSON schema support)
  • System.Text.Json 8.0+ (automatically installed - for JSON serialization)
  • Database provider packages (install as needed):
    • Microsoft.Data.SqlClient for SQL Server
    • Npgsql for PostgreSQL
    • MySqlConnector for MySQL
    • Microsoft.Data.Sqlite for SQLite
  • Optional: Noundry.Tuxedo.Bowtie (for [ForeignKey] attribute support)

Contributing

Contributions are welcome! Please see the main Tuxedo repository for contribution guidelines.

License

Cufflink is part of the Noundry.Tuxedo ecosystem and follows the same license.

See Also

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.

This package has no dependencies.

Version Downloads Last Updated
0.4.0 221 3/4/2026