StructuredChat 0.0.6

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

StructuredChat

Overview

StructuredChat is a .NET library that provides a fluent interface for extracting structured data from AI chat responses using schema validation. It wraps the Microsoft.Extensions.AI IChatClient interface and ensures responses conform to predefined formats through JsonSchema.Net validation.

The library leverages JsonSchema.Net and JsonSchema.Net.Generation for schema creation and validation, allowing you to use schema attributes on your classes or provide custom schemas directly.

Why StructuredChat?

The Problem

When working with Large Language Models (LLMs), responses are inherently unpredictable. Even with careful prompting, models can return:

  • Missing required fields
  • Wrong data types (string instead of number)
  • Values outside valid ranges
  • Inconsistent formats (dates, phone numbers, etc.)
  • Fabricated or hallucinated data

This unpredictability makes it challenging to integrate AI responses into production applications that require reliable, structured data.

The Solution

StructuredChat solves these problems by:

  1. Schema Validation: Every AI response is validated against a JSON schema before being returned to your application
  2. Automatic Retry: When validation fails, the library automatically retries with specific error feedback, helping the AI correct its mistakes
  3. Type Safety: Extract data into strongly-typed C# objects with compile-time safety
  4. Fluent API: Easy-to-use configuration interface that chains naturally
  5. Built-in Schemas: Pre-built schemas for common patterns (yes/no, enums, dates, numbers, etc.)
  6. Custom Schemas: Full support for custom JSON schemas when you need precise control
  7. Workflow Engine: Build complex multi-step AI interactions with branching, loops, and parallel execution
  8. Chat History: Maintain conversation context across multiple interactions

Features

  • Type-safe extraction - Extract structured data with compile-time type safety
  • Schema validation - Automatic validation using JsonSchema.Net
  • Retry mechanism - Built-in retry logic with exponential backoff
  • Fluent API - Easy-to-use configuration interface
  • Multiple data types - Support for enums, numbers, dates, lists, and custom objects
  • Workflow engine - Build complex multi-step workflows with branching and loops
  • Chat history - Maintain conversation context across interactions
  • Multi-framework support - Works with .NET Standard 2.0, .NET 8, 9, and 10

Installation

dotnet add package StructuredChat
<PackageReference Include="StructuredChat" />

Dependencies:

  • Microsoft.Extensions.AI.Abstractions
  • JsonSchema.Net
  • JsonSchema.Net.Generation

Getting Started

First, create an instance of IChatClient (using your preferred AI provider), then wrap it with StructuredChat:

using Microsoft.Extensions.AI;
using StructuredChat;

// Create your AI client (example using OpenAI)
IChatClient chatClient = new OpenAIClient("api-key")
    .GetChatClient("gpt-4o")
    .AsIChatClient();

// Wrap it with StructuredChat
var structuredClient = chatClient.AsStructuredChatClient();

Configuration Options

Configure various parameters using a fluent API:

var client = chatClient.AsStructuredChatClient()
    .WithSystemPrompt("You are a helpful assistant.")
    .WithTemperature(0.7f)
    .WithMaxOutputTokens(1000)
    .WithTopK(40)
    .WithTopP(0.9f)
    .WithFrequencyPenalty(0.5f)
    .WithPresencePenalty(0.5f)
    .WithMaxRetries(5)
    .WithRetryDelay(2000)
    .WithSchemaValidationException(); // Throw exception on validation failure

Configuration Methods

Method Parameter Type Description Default
WithSystemPrompt(string) string Set custom system prompt Data extraction prompt
WithTemperature(float) float Control randomness (0.0 - 2.0) null (provider default)
WithMaxOutputTokens(int) int Limit response length null (provider default)
WithTopK(int) int Limit vocabulary for sampling null (provider default)
WithTopP(float) float Nucleus sampling threshold (0.0 - 1.0) null (provider default)
WithFrequencyPenalty(float) float Reduce repetition (-2.0 to 2.0) null (provider default)
WithPresencePenalty(float) float Encourage topic diversity (-2.0 to 2.0) null (provider default)
WithMaxRetries(int) int Retry attempts on validation failure 3
WithRetryDelay(int) int Delay between retries (milliseconds) 1000
WithSchemaValidationException() - Throw exception on validation failure false

Retry Mechanism

StructuredChat includes a built-in retry mechanism that automatically retries failed schema validations.

How It Works

  1. Automatic Retry: When a response fails schema validation, the library automatically retries
  2. Exponential Backoff: Each retry waits progressively longer (delay × attempt number)
  3. Error Feedback: Validation errors are sent back to the AI model for correction
  4. Configurable: Both retry count and delay are configurable

Error Handling Modes

Default Mode (Returns Invalid Result)

var client = chatClient.AsStructuredChatClient()
    .WithMaxRetries(3);

var result = await client.AskStructuredAsync<Product>("Extract product info");

if (!result.IsValid)
{
    foreach (var error in result.ValidationErrors)
    {
        Console.WriteLine($"{error.PropertyPath}: {string.Join(", ", error.ErrorMessages)}");
    }
}

Exception Mode (Throws on Failure)

var client = chatClient.AsStructuredChatClient()
    .WithMaxRetries(3)
    .WithSchemaValidationException();

try
{
    var result = await client.AskStructuredAsync<Product>("Extract product info");
    // Use result.Data safely - guaranteed to be valid
}
catch (SchemaValidationException ex)
{
    Console.WriteLine($"Validation failed after {ex.ValidationErrors.Count} errors");
    foreach (var error in ex.ValidationErrors)
    {
        Console.WriteLine($"{error.PropertyPath}: {string.Join(", ", error.ErrorMessages)}");
    }
}

Ask Methods Reference

1. AskAsync

Ask a generic question and get a text response.

Signature:

Task<StringResult> AskAsync(string question, List<ChatMessage>? history = null)

Returns: StringResult

  • Answer (string?): The text response
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Example:

var result = await client.AskAsync("What is the capital of France?");
Console.WriteLine($"Answer: {result.Answer}"); // "Paris"

2. AskYesNoAsync

Extract boolean answers from natural language questions.

Signature:

Task<YesNoResult> AskYesNoAsync(string question, List<ChatMessage>? history = null)

Returns: YesNoResult

  • Answer (bool): True for "yes", False for "no"
  • RawResponse (string): The raw "yes" or "no" response
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Example:

var result = await client.AskYesNoAsync("Is the sky blue?");

Console.WriteLine($"Answer: {result.Answer}"); // True
Console.WriteLine($"Raw Response: {result.RawResponse}"); // "yes"

// More examples
var isRaining = await client.AskYesNoAsync("Based on the forecast, will it rain tomorrow?");
var isAffordable = await client.AskYesNoAsync("Is $50,000 an affordable price for a luxury car?");

3. AskSingleChoiceAsync

Select one option from a predefined list.

Signature:

Task<SingleChoiceResult<T>> AskSingleChoiceAsync<T>(
    string question, 
    IEnumerable<T> choices,
    List<ChatMessage>? history = null
) where T : notnull

Returns: SingleChoiceResult<T>

  • Choice (T?): The selected choice
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// String choices
var colors = new[] { "Red", "Blue", "Green", "Yellow" };
var result = await client.AskSingleChoiceAsync(
    "What color is the ocean?", 
    colors
);
Console.WriteLine($"Selected: {result.Choice}"); // "Blue"

// Numeric choices
var numbers = new[] { 1, 2, 3, 4, 5 };
var primeResult = await client.AskSingleChoiceAsync(
    "Pick the smallest prime number", 
    numbers
);
Console.WriteLine($"Selected: {primeResult.Choice}"); // 2

// Custom object choices
var options = new[] 
{ 
    new { Id = 1, Name = "Basic" },
    new { Id = 2, Name = "Premium" },
    new { Id = 3, Name = "Enterprise" }
};
var planResult = await client.AskSingleChoiceAsync(
    "Which plan offers the most features?",
    options
);

4. AskMultipleChoiceAsync

Select multiple options from a list with optional min/max constraints.

Signature:

Task<MultipleChoiceResult<T>> AskMultipleChoiceAsync<T>(
    string question, 
    IEnumerable<T> choices,
    int? minSelections = null, 
    int? maxSelections = null,
    List<ChatMessage>? history = null
) where T : notnull

Returns: MultipleChoiceResult<T>

  • Choices (List<T>): The selected choices
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Without constraints
var fruits = new[] { "Apple", "Banana", "Orange", "Grape", "Mango" };
var result = await client.AskMultipleChoiceAsync(
    "Which fruits are citrus?",
    fruits
);
foreach (var choice in result.Choices)
{
    Console.WriteLine($"- {choice}"); // Orange
}

// With minimum selections
var languages = new[] { "C#", "Java", "Python", "JavaScript", "Go" };
var langResult = await client.AskMultipleChoiceAsync(
    "Which languages support garbage collection?",
    languages,
    minSelections: 1
);

// With both constraints
var features = new[] { "Authentication", "Logging", "Caching", "Monitoring", "Queue" };
var featureResult = await client.AskMultipleChoiceAsync(
    "Select essential features for a production API",
    features,
    minSelections: 2,
    maxSelections: 4
);

5. AskEnumAsync

Extract enum values from responses.

Signature:

Task<EnumResult<TEnum>> AskEnumAsync<TEnum>(
    string question,
    List<ChatMessage>? history = null
) where TEnum : struct, Enum

Returns: EnumResult<TEnum>

  • Value (TEnum?): The extracted enum value
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

public enum Priority
{
    Low,
    Medium,
    High,
    Critical
}

var result = await client.AskEnumAsync<Priority>(
    "What priority should we assign to a security breach?"
);
Console.WriteLine($"Priority: {result.Value}"); // Priority.Critical

public enum DayOfWeek
{
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

var dayResult = await client.AskEnumAsync<DayOfWeek>(
    "What day comes after Wednesday?"
);
Console.WriteLine($"Day: {dayResult.Value}"); // DayOfWeek.Thursday

public enum LogLevel
{
    Trace, Debug, Information, Warning, Error, Critical
}

var logResult = await client.AskEnumAsync<LogLevel>(
    "What log level should we use for exceptions?"
);

6. AskSingleWordAsync

Extract exactly one word (no whitespace allowed).

Signature:

Task<SingleWordResult> AskSingleWordAsync(
    string question,
    List<ChatMessage>? history = null
)

Returns: SingleWordResult

  • Word (string?): The extracted single word
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

var result = await client.AskSingleWordAsync(
    "What is the capital of France?"
);
Console.WriteLine($"Word: {result.Word}"); // "Paris"

var colorResult = await client.AskSingleWordAsync(
    "What color do you get when you mix blue and yellow?"
);
Console.WriteLine($"Color: {colorResult.Word}"); // "Green"

var verbResult = await client.AskSingleWordAsync(
    "What is the past tense of 'run'?"
);
Console.WriteLine($"Verb: {verbResult.Word}"); // "Ran"

7. AskStringAsync

Extract a string response with word count constraints.

Signature:

Task<StringResult> AskStringAsync(
    string question, 
    int minWords, 
    int maxWords,
    List<ChatMessage>? history = null
)

Returns: StringResult

  • Answer (string?): The text response
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Get a brief summary (5-20 words)
var result = await client.AskStringAsync(
    "Summarize the benefits of exercise",
    minWords: 5,
    maxWords: 20
);
Console.WriteLine($"Summary: {result.Answer}");

// Get a detailed explanation (50-100 words)
var detailed = await client.AskStringAsync(
    "Explain how photosynthesis works",
    minWords: 50,
    maxWords: 100
);

8. AskWithRegexPatternAsync

Extract data matching a specific regex pattern.

Signature:

Task<RegexResult> AskWithRegexPatternAsync(
    string question, 
    string regexPattern, 
    string patternDescription = "",
    List<ChatMessage>? history = null
)

Returns: RegexResult

  • Value (string): The extracted value matching the pattern
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Extract email address
var emailResult = await client.AskWithRegexPatternAsync(
    "What is the support email? Contact us at support@example.com",
    @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
    "Valid email address format"
);
Console.WriteLine($"Email: {emailResult.Value}"); // "support@example.com"

// Extract phone number
var phoneResult = await client.AskWithRegexPatternAsync(
    "Call us at (555) 123-4567",
    @"^\(\d{3}\) \d{3}-\d{4}$",
    "US phone number format (XXX) XXX-XXXX"
);
Console.WriteLine($"Phone: {phoneResult.Value}"); // "(555) 123-4567"

// Extract ISBN
var isbnResult = await client.AskWithRegexPatternAsync(
    "Book ISBN: 978-0-123456-78-9",
    @"^\d{3}-\d{1}-\d{6}-\d{2}-\d{1}$",
    "ISBN-13 format"
);
Console.WriteLine($"ISBN: {isbnResult.Value}"); // "978-0-123456-78-9"

// Extract hex color code
var colorResult = await client.AskWithRegexPatternAsync(
    "The primary color is #FF5733",
    @"^#[0-9A-Fa-f]{6}$",
    "Hex color code"
);
Console.WriteLine($"Color: {colorResult.Value}"); // "#FF5733"

// Extract version number
var versionResult = await client.AskWithRegexPatternAsync(
    "Current version: v2.3.1",
    @"^v?\d+\.\d+\.\d+$",
    "Semantic version format"
);

9. AskNumberAsync

Extract numeric values with optional min/max constraints.

Signature:

Task<NumberResult> AskNumberAsync(
    string question, 
    double? minimum = null, 
    double? maximum = null,
    List<ChatMessage>? history = null
)

Returns: NumberResult

  • Value (double?): The extracted number
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Without constraints
var result = await client.AskNumberAsync(
    "How many days are in a week?"
);
Console.WriteLine($"Number: {result.Value}"); // 7

// With minimum
var ageResult = await client.AskNumberAsync(
    "What is the minimum voting age in the US?",
    minimum: 0
);
Console.WriteLine($"Age: {ageResult.Value}"); // 18

// With maximum
var percentResult = await client.AskNumberAsync(
    "What percentage of Earth is covered by water?",
    maximum: 100
);
Console.WriteLine($"Percent: {percentResult.Value}"); // ~71

// With both constraints
var scoreResult = await client.AskNumberAsync(
    "Rate this product on a scale of 1-10",
    minimum: 1,
    maximum: 10
);
Console.WriteLine($"Score: {scoreResult.Value}"); // e.g., 8.5

// Decimal values
var priceResult = await client.AskNumberAsync(
    "What is the average price of coffee in USD?",
    minimum: 0,
    maximum: 20
);
Console.WriteLine($"Price: ${priceResult.Value}"); // e.g., 4.50

10. AskNumberRangeAsync

Extract a range of numbers (minimum and maximum values).

Signature:

Task<NumberRangeResult> AskNumberRangeAsync(
    string question, 
    double? absoluteMinimum = null, 
    double? absoluteMaximum = null,
    List<ChatMessage>? history = null
)

Returns: NumberRangeResult

  • Minimum (double?): The lower bound of the range
  • Maximum (double?): The upper bound of the range
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Without constraints
var result = await client.AskNumberRangeAsync(
    "What is the normal human body temperature range in Celsius?"
);
Console.WriteLine($"Range: {result.Minimum}°C - {result.Maximum}°C"); 
// e.g., 36.5 - 37.5

// With absolute constraints
var salaryResult = await client.AskNumberRangeAsync(
    "What is the typical salary range for a software engineer in USD?",
    absoluteMinimum: 0,
    absoluteMaximum: 1000000
);
Console.WriteLine($"Salary Range: ${salaryResult.Minimum} - ${salaryResult.Maximum}");
// e.g., 70000 - 150000

// Temperature range
var tempResult = await client.AskNumberRangeAsync(
    "What is the ideal temperature range for wine storage in Fahrenheit?",
    absoluteMinimum: 0,
    absoluteMaximum: 100
);

// Age range
var ageRangeResult = await client.AskNumberRangeAsync(
    "What age range is considered 'middle age'?",
    absoluteMinimum: 0,
    absoluteMaximum: 120
);

11. AskDateTimeAsync

Extract dates and times in ISO 8601 format.

Signature:

Task<DateTimeResult> AskDateTimeAsync(
    string question,
    List<ChatMessage>? history = null
)

Returns: DateTimeResult

  • DateTime (DateTime?): The extracted date/time
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

var result = await client.AskDateTimeAsync(
    "When did World War II end in Europe?"
);
Console.WriteLine($"DateTime: {result.DateTime}"); 
// 1945-05-08T00:00:00

var moonResult = await client.AskDateTimeAsync(
    "When did humans first land on the moon?"
);
Console.WriteLine($"Moon Landing: {moonResult.DateTime:yyyy-MM-dd}");
// 1969-07-20

var independenceResult = await client.AskDateTimeAsync(
    "When did the United States declare independence?"
);
Console.WriteLine($"Independence: {independenceResult.DateTime}");
// 1776-07-04T00:00:00

// With specific times
var launchResult = await client.AskDateTimeAsync(
    "When did Apollo 11 launch? (Include time)"
);
// 1969-07-16T13:32:00Z

12. AskDateTimeRangeAsync

Extract a date/time range (start and end).

Signature:

Task<DateTimeRangeResult> AskDateTimeRangeAsync(
    string question,
    List<ChatMessage>? history = null
)

Returns: DateTimeRangeResult

  • StartDate (DateTime?): The start date/time
  • EndDate (DateTime?): The end date/time
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

var result = await client.AskDateTimeRangeAsync(
    "What was the duration of the Renaissance period?"
);
Console.WriteLine($"Start: {result.StartDate}"); // ~1300-01-01
Console.WriteLine($"End: {result.EndDate}");     // ~1600-01-01

var warResult = await client.AskDateTimeRangeAsync(
    "What were the start and end dates of World War I?"
);
Console.WriteLine($"WWI: {warResult.StartDate:yyyy-MM-dd} to {warResult.EndDate:yyyy-MM-dd}");
// 1914-07-28 to 1918-11-11

var projectResult = await client.AskDateTimeRangeAsync(
    "The project runs from January 15, 2024 to March 30, 2024. What are the dates?"
);

var vacationResult = await client.AskDateTimeRangeAsync(
    "Extract vacation dates: We're traveling from Dec 20, 2024 through Jan 5, 2025"
);

13. AskListAsync (Strings)

Extract a list of strings with optional size constraints.

Signature:

Task<ListResult<string>> AskListAsync(
    string question, 
    int? minItems = null, 
    int? maxItems = null,
    List<ChatMessage>? history = null
)

Returns: ListResult<string>

  • Items (List<string>): The extracted list of strings
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Without constraints
var result = await client.AskListAsync(
    "List the planets in our solar system"
);
foreach (var planet in result.Items)
{
    Console.WriteLine($"- {planet}");
}
// Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune

// With minimum items
var colorsResult = await client.AskListAsync(
    "List primary colors",
    minItems: 3
);
// Red, Blue, Yellow

// With maximum items
var topResult = await client.AskListAsync(
    "List popular programming languages",
    maxItems: 5
);

// With both constraints
var featuresResult = await client.AskListAsync(
    "List essential features for a web application",
    minItems: 3,
    maxItems: 7
);

14. AskListAsync (Generic)

Extract a list of complex objects.

Signature:

Task<ListResult<T>> AskListAsync<T>(
    string question, 
    int? minItems = null, 
    int? maxItems = null,
    List<ChatMessage>? history = null
)

Returns: ListResult<T>

  • Items (List<T>): The extracted list of objects
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
    public int Year { get; set; }
}

var result = await client.AskListAsync<Book>(
    "List three classic novels with their authors and publication years",
    minItems: 3,
    maxItems: 3
);

foreach (var book in result.Items)
{
    Console.WriteLine($"{book.Title} by {book.Author} ({book.Year})");
}
// To Kill a Mockingbird by Harper Lee (1960)
// 1984 by George Orwell (1949)
// Pride and Prejudice by Jane Austen (1813)

public class City
{
    public string Name { get; set; }
    public string Country { get; set; }
    public int Population { get; set; }
}

var citiesResult = await client.AskListAsync<City>(
    "List the 5 most populous cities in the world",
    maxItems: 5
);

public class Recipe
{
    public string Name { get; set; }
    public List<string> Ingredients { get; set; }
    public int PrepTimeMinutes { get; set; }
}

var recipesResult = await client.AskListAsync<Recipe>(
    "Suggest 3 quick breakfast recipes",
    minItems: 3,
    maxItems: 3
);

15. AskStructuredAsync (Auto Schema)

Extract complex structured data using custom classes. The schema is automatically generated from the class definition.

Signature:

Task<StructuredResult<T>> AskStructuredAsync<T>(
    string question,
    List<ChatMessage>? history = null
) where T : class

Returns: StructuredResult<T>

  • Data (T?): The extracted structured data
  • HasValue (bool): True if an answer was received
  • IsValid (bool): Schema validation status
  • ValidationErrors (List<SchemaValidationError>): Validation errors if any
  • ChatHistory (List<ChatMessage>): The conversation history

Examples:

// Basic example
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    public List<string> Hobbies { get; set; }
}

var result = await client.AskStructuredAsync<Person>(
    @"Extract information about John: 
      John Doe is 30 years old. 
      His email is john.doe@example.com. 
      He enjoys reading, hiking, and photography."
);

var person = result.Data;
Console.WriteLine($"Name: {person.Name}");
Console.WriteLine($"Age: {person.Age}");
Console.WriteLine($"Email: {person.Email}");
Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}");

// Complex nested example
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

public class Company
{
    public string Name { get; set; }
    public string Industry { get; set; }
    public int EmployeeCount { get; set; }
    public Address HeadquartersAddress { get; set; }
    public List<string> Products { get; set; }
}

var companyResult = await client.AskStructuredAsync<Company>(
    @"Extract company info: 
      TechCorp is a software company with 500 employees. 
      They are located at 123 Main St, San Francisco, CA 94105.
      Their main products are CloudService, DataAnalyzer, and SecuritySuite."
);

16. AskStructuredAsync (Custom JsonSchema)

Extract structured data using a custom JsonSchema.Net schema.

Signature:

Task<StructuredResult<T>> AskStructuredAsync<T>(
    string question, 
    JsonSchema customSchema,
    List<ChatMessage>? history = null
) where T : class

Examples:

using Json.Schema;

// Custom schema with validation rules
var customSchema = new JsonSchemaBuilder()
    .Type(SchemaValueType.Object)
    .Properties(
        ("productName", new JsonSchemaBuilder()
            .Type(SchemaValueType.String)
            .MinLength(3)),
        ("price", new JsonSchemaBuilder()
            .Type(SchemaValueType.Number)
            .Minimum(0)
            .Maximum(10000)),
        ("inStock", new JsonSchemaBuilder()
            .Type(SchemaValueType.Boolean))
    )
    .Required("productName", "price")
    .AdditionalProperties(false)
    .Build();

public class Product
{
    public string ProductName { get; set; }
    public double Price { get; set; }
    public bool InStock { get; set; }
}

var result = await client.AskStructuredAsync<Product>(
    "Extract: The laptop costs $999 and is currently in stock",
    customSchema
);

Console.WriteLine($"{result.Data.ProductName}: ${result.Data.Price}");
Console.WriteLine($"In Stock: {result.Data.InStock}");

17. AskStructuredAsync (Schema as JSON String)

Extract structured data using a JSON schema provided as a string.

Signature:

Task<StructuredResult<T>> AskStructuredAsync<T>(
    string question, 
    string schemaJson,
    List<ChatMessage>? history = null
) where T : class

Examples:

public class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
    public bool Available { get; set; }
}

// Define schema as JSON string
var schemaJson = """
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 3
    },
    "price": {
      "type": "number",
      "minimum": 0,
      "maximum": 10000
    },
    "available": {
      "type": "boolean"
    }
  },
  "required": ["name", "price"],
  "additionalProperties": false
}
""";

var result = await client.AskStructuredAsync<Product>(
    "Extract: Premium Laptop costs $1299 and is available",
    schemaJson
);

Console.WriteLine($"{result.Data.Name}: ${result.Data.Price}");
Console.WriteLine($"Available: {result.Data.Available}");

18. AskStructuredAsync (Schema as Anonymous Object)

Extract structured data using a JSON schema provided as an anonymous object.

Signature:

Task<StructuredResult<T>> AskStructuredAsync<T>(
    string question, 
    object schemaObject,
    List<ChatMessage>? history = null
) where T : class

Examples:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

// Define schema using anonymous object
var schemaObject = new
{
    type = "object",
    properties = new
    {
        name = new
        {
            type = "string",
            minLength = 2,
            maxLength = 100
        },
        age = new
        {
            type = "integer",
            minimum = 0,
            maximum = 150
        },
        email = new
        {
            type = "string",
            pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
        }
    },
    required = new[] { "name", "age", "email" },
    additionalProperties = false
};

var result = await client.AskStructuredAsync<Person>(
    "Extract: Alice Smith, 28 years old, email: alice@example.com",
    schemaObject
);

Console.WriteLine($"{result.Data.Name}, {result.Data.Age}, {result.Data.Email}");

19. AskStructuredAsync (Schema as Dictionary)

Extract structured data using a JSON schema provided as a dictionary.

Signature:

Task<StructuredResult<T>> AskStructuredAsync<T>(
    string question, 
    Dictionary<string, object> schemaDictionary,
    List<ChatMessage>? history = null
) where T : class

Examples:

public class Book
{
    public string Isbn { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public int YearPublished { get; set; }
}

// Define schema using Dictionary
var schemaDictionary = new Dictionary<string, object>
{
    ["type"] = "object",
    ["properties"] = new Dictionary<string, object>
    {
        ["isbn"] = new Dictionary<string, object>
        {
            ["type"] = "string",
            ["pattern"] = "^[0-9]{3}-[0-9]{10}$"
        },
        ["title"] = new Dictionary<string, object>
        {
            ["type"] = "string",
            ["minLength"] = 1
        },
        ["author"] = new Dictionary<string, object>
        {
            ["type"] = "string"
        },
        ["yearPublished"] = new Dictionary<string, object>
        {
            ["type"] = "integer",
            ["minimum"] = 1000,
            ["maximum"] = 2100
        }
    },
    ["required"] = new[] { "isbn", "title", "author", "yearPublished" },
    ["additionalProperties"] = false
};

var result = await client.AskStructuredAsync<Book>(
    "Extract: ISBN 978-0123456789, '1984' by George Orwell, published 1949",
    schemaDictionary
);

Console.WriteLine($"{result.Data.Title} by {result.Data.Author} ({result.Data.YearPublished})");

Working with JsonSchema.Net Attributes

StructuredChat fully supports JsonSchema.Net.Generation attributes for fine-grained schema control.

Available Attributes

using Json.Schema.Generation;

public class AdvancedPerson
{
    [Required]
    [MinLength(2)]
    [MaxLength(100)]
    public string Name { get; set; }

    [Minimum(0)]
    [Maximum(150)]
    public int Age { get; set; }

    [Pattern(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
    public string Email { get; set; }

    [MinItems(1)]
    [MaxItems(10)]
    [UniqueItems(true)]
    public List<string> Skills { get; set; }

    [Description("Person's biography or description")]
    [MinLength(10)]
    public string Bio { get; set; }

    [Title("Contact Number")]
    [Pattern(@"^\+?[1-9]\d{1,14}$")]
    public string PhoneNumber { get; set; }
}

var result = await client.AskStructuredAsync<AdvancedPerson>(
    "Extract: John Smith, 35 years old, email john@example.com..."
);

Common JsonSchema.Net Attributes

Attribute Applies To Description
[Required] Properties Marks property as required
[Description("...")] All Adds description for AI context
[Title("...")] All Sets display title
[MinLength(n)] String Minimum string length
[MaxLength(n)] String Maximum string length
[Pattern("regex")] String Regex pattern validation
[Minimum(n)] Number Minimum numeric value
[Maximum(n)] Number Maximum numeric value
[ExclusiveMinimum(n)] Number Exclusive minimum value
[ExclusiveMaximum(n)] Number Exclusive maximum value
[MultipleOf(n)] Number Value must be multiple of n
[MinItems(n)] Array Minimum array length
[MaxItems(n)] Array Maximum array length
[UniqueItems(true)] Array Array items must be unique

Chat History

StructuredChat supports maintaining conversation context across multiple interactions. This is useful for:

  • Follow-up questions
  • Multi-turn conversations
  • Context-aware responses

Manual History Management

var history = new List<ChatMessage>();

// First question
var result1 = await client.AskAsync("What is the capital of France?", history);
// history now contains the conversation

// Follow-up question using the same history
var result2 = await client.AskAsync("What is its population?", history);
// AI understands "its" refers to Paris from previous context

// Continue the conversation
var result3 = await client.AskYesNoAsync("Is it larger than London?", history);

Accessing Chat History from Results

Every result includes the chat history:

var result = await client.AskAsync("Tell me about machine learning");

// Access the history from the result
foreach (var message in result.ChatHistory)
{
    Console.WriteLine($"{message.Role}: {message.Text}");
}

Workflow Engine

The Workflow Engine allows you to build complex, multi-step AI interactions with branching logic, loops, parallel execution, and data transformation.

Creating a Workflow

var workflow = client.CreateWorkflow()
    .AskYesNo("interested", "Are you interested in technology?")
    .If(
        ctx => ctx.Get<YesNoResult>("interested")?.Answer == true,
        then => then
            .AskSingleChoice("language", "What's your favorite programming language?", 
                new[] { "C#", "Python", "JavaScript", "Go" })
            .AskNumber("experience", "How many years of experience do you have?", 
                minimum: 0, maximum: 50),
        @else => @else
            .Ask("alternative", "What field are you interested in instead?")
    )
    .Build();

var context = await workflow.ExecuteAsync();

// Access results
var isInterested = context.Get<YesNoResult>("interested");
var language = context.Get<SingleChoiceResult<string>>("language");

Workflow with Maintained History

Enable conversation history across all workflow steps:

var workflow = client.CreateWorkflow(maintainHistory: true)
    .Ask("intro", "Introduce yourself briefly")
    .Ask("followup", "What are your main skills?")
    .Ask("detail", "Tell me more about your most impressive project")
    .Build();

var context = await workflow.ExecuteAsync();
// Each step has access to the previous conversation context

Dynamic Questions

Use context data to generate dynamic questions:

var workflow = client.CreateWorkflow()
    .AskSingleWord("name", "What is your first name?")
    .Ask("greeting", ctx => $"Hello {ctx.Get<SingleWordResult>("name")?.Word}! How are you today?")
    .AskNumber("rating", ctx => 
        $"On a scale of 1-10, how would you rate your day, {ctx.Get<SingleWordResult>("name")?.Word}?",
        minimum: 1, maximum: 10)
    .Build();

Conditional Branching (If/Else)

var workflow = client.CreateWorkflow()
    .AskNumber("age", "What is your age?", minimum: 0, maximum: 120)
    .If(
        ctx => ctx.Get<NumberResult>("age")?.Value >= 18,
        then => then
            .Ask("adult", "What are your career goals?"),
        @else => @else
            .Ask("minor", "What do you want to be when you grow up?")
    )
    .Build();

Switch Statement

public enum Department { Engineering, Sales, HR, Marketing }

var workflow = client.CreateWorkflow()
    .AskEnum<Department>("dept", "Which department do you work in?")
    .Switch(
        ctx => ctx.Get<EnumResult<Department>>("dept")?.Value ?? Department.Engineering,
        cases =>
        {
            cases.Case(Department.Engineering, b => b
                .AskList("skills", "List your technical skills", minItems: 3));
            
            cases.Case(Department.Sales, b => b
                .AskNumber("quota", "What is your monthly sales quota?", minimum: 0));
            
            cases.Case(Department.HR, b => b
                .AskMultipleChoice("focus", "Select your HR focus areas",
                    new[] { "Recruiting", "Training", "Benefits", "Compliance" }));
            
            cases.Default(b => b
                .Ask("general", "Describe your role"));
        }
    )
    .Build();

Loops

For Loop
var workflow = client.CreateWorkflow()
    .For(0, 3, (builder, index) =>
    {
        builder.Ask($"item_{index}", $"Enter item #{index + 1}");
    })
    .Build();

var context = await workflow.ExecuteAsync();
var item0 = context.Get<StringResult>("item_0");
var item1 = context.Get<StringResult>("item_1");
var item2 = context.Get<StringResult>("item_2");
ForEach Loop
var workflow = client.CreateWorkflow()
    .AskList("topics", "List 3 topics you want to learn about", minItems: 3, maxItems: 3)
    .Do(ctx => 
    {
        var topics = ctx.Get<ListResult<string>>("topics")?.Items ?? new List<string>();
        ctx.Set("topicList", topics);
    })
    .ForEach<string>("topicList", "currentTopic", builder =>
    {
        builder.Ask("explanation", ctx => 
            $"Explain {ctx.Get<string>("currentTopic")} in simple terms");
    })
    .Build();
While Loop
var workflow = client.CreateWorkflow()
    .Do(ctx => ctx.Set("attempts", 0))
    .While(
        ctx => ctx.Get<int>("attempts") < 3,
        builder => builder
            .AskNumber("guess", "Guess a number between 1 and 10", minimum: 1, maximum: 10)
            .Do(ctx =>
            {
                var attempts = ctx.Get<int>("attempts");
                ctx.Set("attempts", attempts + 1);
            })
    )
    .Build();

Parallel Execution

Execute multiple branches simultaneously:

var workflow = client.CreateWorkflow()
    .Parallel(
        branch1 => branch1
            .AskList("benefits", "List 3 benefits of exercise", maxItems: 3),
        branch2 => branch2
            .AskList("risks", "List 3 risks of a sedentary lifestyle", maxItems: 3),
        branch3 => branch3
            .AskNumber("hours", "Recommended weekly exercise hours?", minimum: 0, maximum: 40)
    )
    .Build();

var context = await workflow.ExecuteAsync();
// All three branches executed in parallel

Data Transformation

Transform data between steps:

var workflow = client.CreateWorkflow()
    .AskNumber("celsius", "What is the temperature in Celsius?")
    .Transform<NumberResult, double>(
        "celsius", 
        "fahrenheit", 
        result => (result.Value ?? 0) * 9 / 5 + 32
    )
    .Do(ctx =>
    {
        var fahrenheit = ctx.Get<double>("fahrenheit");
        Console.WriteLine($"That's {fahrenheit}°F");
    })
    .Build();

Custom Actions

Synchronous Action
var workflow = client.CreateWorkflow()
    .AskStructured<Person>("person", "Extract person info from: John, 30, john@email.com")
    .Do(ctx =>
    {
        var person = ctx.Get<StructuredResult<Person>>("person")?.Data;
        if (person != null)
        {
            // Log, save to database, etc.
            Console.WriteLine($"Processing: {person.Name}");
            ctx.Set("processed", true);
        }
    })
    .Build();
Asynchronous Action
var workflow = client.CreateWorkflow()
    .AskStructured<Order>("order", "Extract order details...")
    .DoAsync(async ctx =>
    {
        var order = ctx.Get<StructuredResult<Order>>("order")?.Data;
        if (order != null)
        {
            // Async operations like API calls, database saves
            await SaveToDatabase(order);
            await SendNotification(order);
        }
    })
    .Build();

Complete Workflow Example

Here's a comprehensive example showing multiple features together:

public class CustomerInfo
{
    public string Name { get; set; }
    public string Email { get; set; }
    public string Company { get; set; }
}

public enum ProductCategory { Software, Hardware, Services, Consulting }

var workflow = client.CreateWorkflow(maintainHistory: true)
    // Step 1: Gather customer information
    .AskStructured<CustomerInfo>("customer", 
        "Please provide your name, email, and company name")
    
    // Step 2: Determine product interest
    .AskEnum<ProductCategory>("category", 
        ctx => $"Hello {ctx.Get<StructuredResult<CustomerInfo>>("customer")?.Data?.Name}! " +
               "What category of products are you interested in?")
    
    // Step 3: Branch based on category
    .Switch(
        ctx => ctx.Get<EnumResult<ProductCategory>>("category")?.Value ?? ProductCategory.Software,
        cases =>
        {
            cases.Case(ProductCategory.Software, b => b
                .AskMultipleChoice("features", "Select desired features",
                    new[] { "Cloud", "On-premise", "Mobile", "API", "Analytics" },
                    minSelections: 1, maxSelections: 3)
                .AskNumber("users", "Expected number of users?", minimum: 1, maximum: 100000));
            
            cases.Case(ProductCategory.Hardware, b => b
                .AskSingleChoice("type", "What type of hardware?",
                    new[] { "Servers", "Networking", "Storage", "Workstations" })
                .AskNumberRange("budget", "What is your budget range in USD?",
                    absoluteMinimum: 1000, absoluteMaximum: 1000000));
            
            cases.Case(ProductCategory.Services, b => b
                .AskList("services", "List the services you need", minItems: 1, maxItems: 5));
            
            cases.Default(b => b
                .Ask("consulting", "Describe your consulting needs"));
        }
    )
    
    // Step 4: Timeline
    .AskDateTimeRange("timeline", "When do you need the solution implemented?")
    
    // Step 5: Final confirmation
    .AskYesNo("confirm", ctx =>
    {
        var customer = ctx.Get<StructuredResult<CustomerInfo>>("customer")?.Data;
        return $"Thank you {customer?.Name}! Would you like us to contact you at {customer?.Email}?";
    })
    
    // Step 6: Process based on confirmation
    .If(
        ctx => ctx.Get<YesNoResult>("confirm")?.Answer == true,
        then => then
            .DoAsync(async ctx =>
            {
                var customer = ctx.Get<StructuredResult<CustomerInfo>>("customer")?.Data;
                // Send confirmation email, create CRM entry, etc.
                await SendConfirmationEmail(customer);
                ctx.Set("status", "Lead captured successfully");
            }),
        @else => @else
            .Do(ctx => ctx.Set("status", "Customer declined contact"))
    )
    .Build();

// Execute the workflow
var context = await workflow.ExecuteAsync();

// Access all collected data
var customer = context.Get<StructuredResult<CustomerInfo>>("customer")?.Data;
var category = context.Get<EnumResult<ProductCategory>>("category")?.Value;
var timeline = context.Get<DateTimeRangeResult>("timeline");
var status = context.Get<string>("status");

Console.WriteLine($"Lead Status: {status}");
Console.WriteLine($"Customer: {customer?.Name} from {customer?.Company}");
Console.WriteLine($"Category: {category}");
Console.WriteLine($"Timeline: {timeline?.StartDate:d} to {timeline?.EndDate:d}");

Workflow Context Data Management

The StructuredWorkflowContext is a thread-safe data container for storing and managing state throughout workflow execution.

Context Methods

Method Signature Description
Set<T> void Set<T>(string key, T value) Store a value with the specified key
Get<T> T? Get<T>(string key) Retrieve a value by key, cast to type T
Contains bool Contains(string key) Check if a key exists in the context
Remove void Remove(string key) Remove a specific key from the context
RemoveAll void RemoveAll() Clear all data from the context

Example

var workflow = client.CreateWorkflow()
    // Store various types
    .Do(ctx => 
    {
        ctx.Set("counter", 0);
        ctx.Set("userName", "John");
        ctx.Set("config", new AppConfig { MaxRetries = 3 });
    })
    
    // Use stored data in questions
    .Ask("greeting", ctx => $"Hello {ctx.Get<string>("userName")}! How can I help?")
    
    // Update values based on results
    .AskNumber("score", "Rate from 1-10", minimum: 1, maximum: 10)
    .Do(ctx =>
    {
        var score = ctx.Get<NumberResult>("score")?.Value ?? 0;
        ctx.Set("grade", score >= 7 ? "Pass" : "Fail");
        ctx.Set("normalizedScore", score / 10.0);
    })
    .Build();

var context = await workflow.ExecuteAsync();
Console.WriteLine($"Grade: {context.Get<string>("grade")}");

Validation Results

All methods return result objects that include validation information:

public class BaseResult
{
    public bool HasValue { get; }
    public bool IsValid { get; }
    public List<SchemaValidationError> ValidationErrors { get; }
    public List<ChatMessage> ChatHistory { get; }
}

Understanding HasValue vs IsValid

  • HasValue: Checks if the response contains actual data

    • true = Data exists (not null, not empty, not default value)
    • false = No data received or empty response
  • IsValid: Checks if the data passes schema validation

    • true = Data conforms to all schema rules and constraints
    • false = Data violates one or more schema requirements
  • ValidationErrors: Detailed error information when validation fails

    • Empty when IsValid is true
    • Contains specific error messages and property paths when IsValid is false

Handling Validation Results

var result = await client.AskStructuredAsync<Product>("...");

if (result.IsValid && result.HasValue)
{
    // Safe to use result.Data
    ProcessProduct(result.Data);
}
else if (!result.IsValid)
{
    // Handle validation errors
    foreach (var error in result.ValidationErrors)
    {
        Console.WriteLine($"Error at {error.PropertyPath}:");
        foreach (var message in error.ErrorMessages)
        {
            Console.WriteLine($"  - {message}");
        }
    }
}
else
{
    // No data received
    Console.WriteLine("No data was extracted");
}

Schema Method Comparison

Method Best For Pros Cons
Auto Schema Simple POCOs with attributes Zero configuration, Type-safe Less flexible
JsonSchema Object Complex validation rules Full JsonSchema.Net features More verbose
JSON String External schema files Easy to store/version No compile-time validation
Anonymous Object Quick inline schemas Clean syntax, Easy to read No IntelliSense
Dictionary Runtime schema generation Fully dynamic Type-unsafe

Contributing

We welcome contributions to make StructuredChat even better!

🌟 Star this repository if you find it useful!

🔧 Pull Requests Welcome

Please feel free to fork the repository and submit a pull request.

📝 Reporting Issues

Found a bug or have a suggestion? Please open an issue with:

  • A clear description of the problem or enhancement
  • Steps to reproduce (for bugs)
  • Sample code demonstrating the issue
  • Expected vs actual behavior

📝 License

This project is licensed under the MIT License.

Copyright (c) 2025 Hamed Fathi

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.0.6 396 11/19/2025
0.0.5 401 11/19/2025
0.0.4 404 11/19/2025
0.0.3 284 11/16/2025
0.0.2 230 11/16/2025