Documentation

Core Libraries

Web Development

Data Access

Data Management

Deployment

Development Tools

Project Templates

Automation & Scheduling

AI & MCP

Noundry Documentation

Welcome to the comprehensive documentation for the Noundry platform. Explore our libraries, components, and tools to accelerate your .NET development.

Getting Started with Templates

Start with our project templates for rapid development.

# Create a new web application
$ dotnet new noundry-app -n MyApp
# Create a new API
$ dotnet new noundry-api -n MyApi
# Add Noundry libraries
$ dotnet add package Noundry.UI
$ dotnet add package Noundry.Tuxedo

Quick Reference

Common patterns and code snippets for rapid development.

  • Authentication Setup
  • Database Configuration
  • UI Components

Noundry.Assertive

A fluent assertion library that makes your tests more readable and expressive.

Installation

$ dotnet add package Noundry.Assertive

Basic Usage

using Noundry.Assertive;

// String assertions
result.Should().Be("expected");
name.Should().NotBeNullOrEmpty();
email.Should().Contain("@");

// Numeric assertions
count.Should().BeGreaterThan(0);
price.Should().BeBetween(10.0, 100.0);

// Collection assertions
items.Should().NotBeEmpty();
users.Should().HaveCount(3);
names.Should().Contain("John");

Advanced Examples

// Object property assertions
user.Should().NotBeNull();
user.Name.Should().Be("John Doe");
user.Age.Should().BeGreaterThan(18);

// Exception assertions
var action = () => service.ThrowException();
action.Should().Throw<ArgumentException>()
    .WithMessage("Invalid argument");

// Async assertions
await asyncResult.Should().CompleteWithin(TimeSpan.FromSeconds(5));

Real-World Testing Scenarios

// Unit Test - Service Layer
[Fact]
public async Task CreateUser_ValidData_ReturnsCreatedUser()
{
    // Arrange
    var request = new CreateUserRequest
    {
        Email = "user@example.com",
        Name = "John Doe",
        Age = 25
    };

    // Act
    var result = await _userService.CreateUserAsync(request);

    // Assert
    result.Should().NotBeNull();
    result.Id.Should().BeGreaterThan(0);
    result.Email.Should().Be(request.Email);
    result.Name.Should().Be(request.Name);
    result.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(2));
}

// Integration Test - API Response
[Fact]
public async Task GetUsers_ReturnsPagedResults()
{
    var response = await _client.GetAsync("/api/users?page=1&pageSize=10");

    response.StatusCode.Should().Be(HttpStatusCode.OK);

    var result = await response.Content.ReadFromJsonAsync<PagedResult<UserDto>>();

    result.Should().NotBeNull();
    result.Items.Should().HaveCountLessOrEqualTo(10);
    result.Items.Should().AllSatisfy(u =>
    {
        u.Email.Should().NotBeNullOrEmpty();
        u.Email.Should().MatchRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
    });
}

Guardian

Lightweight guard clauses for defensive programming and input validation.

Installation

$ dotnet add package Guardian

Basic Usage

using Guardian;

public void ProcessUser(User user, string email)
{
    // Null checks
    Guard.Against.Null(user, nameof(user));
    
    // String validation
    Guard.Against.NullOrEmpty(email, nameof(email));
    Guard.Against.InvalidFormat(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", nameof(email));
    
    // Numeric validation
    Guard.Against.NegativeOrZero(user.Age, nameof(user.Age));
    Guard.Against.OutOfRange(user.Age, 1, 120, nameof(user.Age));
}

Collection Guards

// Collection validation
Guard.Against.NullOrEmpty(items, nameof(items));
Guard.Against.CountOutOfRange(items, 1, 100, nameof(items));

// Custom conditions
Guard.Against.Expression(x => x.StartDate > x.EndDate, request, "Start date must be before end date");

Real-World Validation Patterns

// API Controller - Input Validation
[HttpPost("api/orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
    Guard.Against.Null(request, nameof(request));
    Guard.Against.NullOrEmpty(request.Items, nameof(request.Items));
    Guard.Against.NegativeOrZero(request.CustomerId, nameof(request.CustomerId));

    foreach (var item in request.Items)
    {
        Guard.Against.NegativeOrZero(item.Quantity, nameof(item.Quantity));
        Guard.Against.Negative(item.Price, nameof(item.Price));
    }

    var order = await _orderService.CreateAsync(request);
    return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}

// Domain Model - Business Logic Validation
public class Order
{
    public void ApplyDiscount(decimal discountPercentage)
    {
        Guard.Against.OutOfRange(discountPercentage, 0, 100,
            nameof(discountPercentage));
        Guard.Against.Expression(o => o.Status == OrderStatus.Shipped,
            this, "Cannot apply discount to shipped orders");

        TotalAmount *= (1 - discountPercentage / 100);
    }
}

// Service Layer - Complex Business Rules
public async Task TransferFunds(int fromAccountId, int toAccountId, decimal amount)
{
    Guard.Against.NegativeOrZero(amount, nameof(amount));
    Guard.Against.Expression(x => x == toAccountId, fromAccountId,
        "Cannot transfer to the same account");

    var fromAccount = await _repository.GetAsync(fromAccountId);
    Guard.Against.Null(fromAccount, nameof(fromAccount));
    Guard.Against.Expression(a => a.Balance < amount, fromAccount,
        "Insufficient funds");

    // Perform transfer...
}

Noundry.Sod

Zod-inspired schema validation for .NET with fluent API, type-safe validation, and comprehensive error messages.

Installation

$ dotnet add package Noundry.Sod

Quick Start

using Sod;

// Define a schema
var userSchema = Sod.Object<User>()
    .Field(u => u.Username, Sod.String().Min(3).Max(20))
    .Field(u => u.Email, Sod.String().Email())
    .Field(u => u.Age, Sod.Number().Min(18).Max(100));

// Validate data
var result = userSchema.Parse(userData);
if (result.Success)
{
    Console.WriteLine($"Valid user: {result.Data.Username}");
}

String Validations

var emailSchema = Sod.String().Email();
var urlSchema = Sod.String().Url();
var uuidSchema = Sod.String().Uuid();
var regexSchema = Sod.String().Regex(@"^\d{3}-\d{2}-\d{4}$");

// Transformations
var upperSchema = Sod.String().Trim().ToUpperCase();

Complex Objects

var addressSchema = Sod.Object<Address>()
    .Field(a => a.Street, Sod.String().NonEmpty())
    .Field(a => a.City, Sod.String().NonEmpty())
    .Field(a => a.PostalCode, Sod.String().Regex(@"^\d{5}$"));

var personSchema = Sod.Object<Person>()
    .Field(p => p.Name, Sod.String().Min(1))
    .Field(p => p.Age, Sod.Number().Min(0))
    .Field(p => p.Address, addressSchema)
    .Field(p => p.Tags, Sod.Array(Sod.String()).Optional());

Transforms & Refinements

// Password validation with custom refinements
var passwordSchema = Sod.String()
    .Min(8)
    .Refine(pwd => pwd.Any(char.IsDigit),
        "Password must contain a digit")
    .Refine(pwd => pwd.Any(char.IsUpper),
        "Password must contain an uppercase letter");

// Transform input to uppercase
var upperSchema = Sod.String()
    .Transform(s => s.ToUpper());

// Coerce string to number
var numberSchema = Sod.Coerce.Number();

Noundry.UI

Complete UI component library with Alpine.js integration and Tailwind CSS styling.

Installation

$ dotnet add package Noundry.UI

Button Component

Usage

<!-- Primary Button -->
<noundry-button variant="primary" size="md">
    Click Me
</noundry-button>

<!-- Secondary Button -->
<noundry-button variant="secondary">
    Cancel
</noundry-button>

<!-- Loading State -->
<noundry-button loading="true">
    Processing...
</noundry-button>

Examples

Modal Component

Usage

<noundry-modal id="example-modal">
    <div slot="header">
        <h3>Modal Title</h3>
    </div>
    
    <div slot="body">
        <p>Modal content goes here.</p>
    </div>
    
    <div slot="footer">
        <noundry-button variant="primary">
            Save
        </noundry-button>
    </div>
</noundry-modal>

Example

Confirm Action

Are you sure you want to delete this item?

Form Components

Input Field

<noundry-input
    label="Email Address"
    type="email"
    placeholder="Enter your email"
    required="true"
/>

Example

Noundry Elements

A powerful, lightweight form validation library for modern web applications. Built for vanilla JavaScript and Alpine.js integration.

Installation

# CDN (Quick Start)
<script src="https://unpkg.com/noundryfx/noundry-elements@latest/dist/noundry-elements.min.js"></script>
# NPM
$ npm install noundryfx@noundry-elements

Key Features

✅ Web Component Architecture

Custom <noundry-element> tag for seamless integration

✅ Native HTML5 Validation

Leverages built-in browser validation

✅ Real-time State Tracking

Monitor dirty, valid, and submitting states

✅ Alpine.js Integration

Seamless reactive integration

Basic Usage

<!-- Simple form with validation -->
<noundry-element>
    <form action="/api/contact" method="POST">
        <input
            type="text"
            name="name"
            required
            data-error="Name is required">

        <input
            type="email"
            name="email"
            required
            data-error="Valid email required">

        <button type="submit">Submit</button>
    </form>
</noundry-element>

Alpine.js Integration

<div x-data="{ formData: {}, formState: {} }">
    <noundry-element
        x-ref="myForm"
        @noundry-change="formData = $event.detail.values; formState = $refs.myForm.$noundry">
        <form>
            <input type="text" name="username" x-model="formData.username">
            <button :disabled="!formState.isValid">Submit</button>
        </form>
    </noundry-element>
</div>

Performance & Size

Lightweight (~8KB minified) with zero dependencies. Perfect for modern web applications.

View Full Documentation →

Noundry.Tuxedo

Modern ORM combining the power of Dapper with additional functionality for rapid data access.

Installation

$ dotnet add package Noundry.Tuxedo

Configuration

// Program.cs
builder.Services.AddTuxedoSqlServer(connectionString);

// Or for PostgreSQL
builder.Services.AddTuxedoPostgreSQL(connectionString);

// Or for MySQL
builder.Services.AddTuxedoMySQL(connectionString);

Basic CRUD Operations

using System.Data;
using Noundry.Tuxedo.Contrib; // For GetAsync, InsertAsync, etc.

public class UserService
{
    private readonly IDbConnection _connection;

    public UserService(IDbConnection connection)
    {
        _connection = connection;
    }

    // Get by ID
    public async Task<User> GetByIdAsync(int id)
    {
        return await _connection.GetAsync<User>(id);
    }

    // Get all
    public async Task<IEnumerable<User>> GetAllAsync()
    {
        return await _connection.GetAllAsync<User>();
    }

    // Insert
    public async Task<int> CreateAsync(User user)
    {
        return await _connection.InsertAsync(user);
    }

    // Update
    public async Task<bool> UpdateAsync(User user)
    {
        return await _connection.UpdateAsync(user);
    }

    // Delete
    public async Task<bool> DeleteAsync(int id)
    {
        return await _connection.DeleteAsync<User>(id);
    }
}

Advanced Queries

// Custom queries with parameters
public async Task<IEnumerable<User>> GetActiveUsersAsync()
{
    var sql = "SELECT * FROM Users WHERE IsActive = @IsActive";
    return await _connection.QueryAsync<User>(sql, new { IsActive = true });
}

// Multi-table join query
public async Task<IEnumerable<OrderWithCustomer>> GetOrdersWithCustomersAsync()
{
    var sql = @"SELECT o.*, c.*
        FROM Orders o
        INNER JOIN Customers c ON o.CustomerId = c.Id
        WHERE o.Status = @Status";
    return await _connection.QueryAsync<OrderWithCustomer>(sql, new { Status = "Active" });
}

// Transactions
public async Task TransferFundsAsync(int fromId, int toId, decimal amount)
{
    if (_connection.State != ConnectionState.Open)
        await _connection.OpenAsync();

    using var transaction = _connection.BeginTransaction();
    try
    {
        await _connection.ExecuteAsync("UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @Id",
            new { Amount = amount, Id = fromId }, transaction);

        await _connection.ExecuteAsync("UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @Id",
            new { Amount = amount, Id = toId }, transaction);

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Exception Translation

Strongly-typed database exceptions for precise error handling. Transform generic DbException errors into specific, catchable exception types:

Basic Usage

using Noundry.Tuxedo.Exceptions;

// Auto-detect database provider
var wrappedConn = connection.WithExceptionTranslation();

// Or specify explicitly
var sqlServerConn = new SqlConnection(connectionString)
    .WithSqlServerExceptionTranslation();

try
{
    await wrappedConn.InsertAsync(user);
}
catch (UniqueConstraintException ex)
{
    // Access detailed constraint information
    if (ex.ColumnName?.Contains("Email") == true)
        return "Email already registered";

    return $"Duplicate value for {ex.ColumnName}";
}
catch (CannotInsertNullException ex)
{
    return $"{ex.ColumnName} is required";
}
catch (ReferenceConstraintException ex)
{
    return "Invalid foreign key reference";
}
catch (MaxLengthExceededException ex)
{
    return $"{ex.ColumnName} exceeds maximum length";
}

User Registration with Exception Translation

public async Task<Result> RegisterUserAsync(UserRegistration registration)
{
    var wrappedConn = _connection.WithExceptionTranslation();

    try
    {
        var user = new User
        {
            Email = registration.Email,
            Username = registration.Username,
            PasswordHash = HashPassword(registration.Password)
        };

        await wrappedConn.InsertAsync(user);
        return Result.Success();
    }
    catch (UniqueConstraintException ex) when (ex.ColumnName?.Contains("Email") == true)
    {
        return Result.Error("This email address is already registered");
    }
    catch (UniqueConstraintException ex) when (ex.ColumnName?.Contains("Username") == true)
    {
        return Result.Error("This username is already taken");
    }
    catch (CannotInsertNullException ex)
    {
        return Result.Error($"{ex.ColumnName} is required");
    }
}

Combine with Resiliency

using Noundry.Tuxedo.Resiliency;
using Noundry.Tuxedo.Exceptions;
using Polly;

var retryPolicy = Policy
    .Handle<Exception>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(attempt));

// Exception translation + Retry policy (order doesn't matter)
var connection = new SqlConnection(connectionString)
    .WithExceptionTranslation()
    .WithRetryPolicy(retryPolicy);

Install: dotnet add package Noundry.Tuxedo.Exceptions

Exception Types: UniqueConstraintException, ReferenceConstraintException, CannotInsertNullException, MaxLengthExceededException, NumericOverflowException

Supported Databases: SQL Server, PostgreSQL, MySQL, SQLite

Real-World Data Operations

Production-ready examples for complex data scenarios:

Bulk Insert with Validation

public async Task<BulkOperationResult> ImportProductsAsync(List<Product> products)
{
    var wrappedConn = _connection.WithExceptionTranslation();
    var result = new BulkOperationResult();

    using var transaction = await wrappedConn.BeginTransactionAsync();
    try
    {
        foreach (var product in products)
        {
            try
            {
                await wrappedConn.InsertAsync(product, transaction);
                result.SuccessCount++;
            }
            catch (UniqueConstraintException ex)
            {
                result.Errors.Add($"Duplicate SKU: {product.SKU}");
                result.FailureCount++;
            }
            catch (MaxLengthExceededException ex)
            {
                result.Errors.Add($"{ex.ColumnName} exceeds max length: {product.Name}");
                result.FailureCount++;
            }
        }

        await transaction.CommitAsync();
        return result;
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Strongly-Typed Entity Operations

using Noundry.Tuxedo.Contrib; // Entity extensions

// Get entity by ID
var product = await _connection.GetAsync<Product>(123);

// Get all entities
var allProducts = await _connection.GetAllAsync<Product>();

// Insert entity and get generated ID
var newProduct = new Product
{
    Name = "Laptop",
    Price = 999.99m,
    Category = "Electronics",
    Stock = 50
};
var productId = await _connection.InsertAsync(newProduct);

// Update entire entity
product.Price = 899.99m;
product.Stock = 45;
await _connection.UpdateAsync(product);

// Partial update - only specific properties
product.Price = 849.99m;
await _connection.UpdateAsync(product,
    propertiesToUpdate: new[] { "Price" });

// Update by key values (no need to fetch first)
await _connection.UpdateAsync<Product>(
    keyValues: new { Id = 123 },
    updateValues: new { Stock = 40, LastModified = DateTime.UtcNow });

// Delete entity
await _connection.DeleteAsync(product);

// Delete all entities of type
await _connection.DeleteAllAsync<TempProduct>();

Dynamic QueryBuilder with LINQ Expressions

using Noundry.Tuxedo.QueryBuilder;

// Basic query with strongly-typed expressions
var electronics = await QueryBuilder.Query<Product>()
    .Where(p => p.Category == "Electronics")
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Price)
    .Take(10)
    .ToListAsync(_connection);

// WhereIn for multiple values
var categoryIds = new[] { 1, 2, 3, 4 };
var filtered = await QueryBuilder.Query<Product>()
    .WhereIn(p => p.CategoryId, categoryIds)
    .WhereNotIn(p => p.StatusId, new[] { 99, 100 })
    .ToListAsync(_connection);

// WhereBetween for range queries
var priceRange = await QueryBuilder.Query<Product>()
    .WhereBetween(p => p.Price, 10.00m, 99.99m)
    .WhereBetween(p => p.CreatedDate, startDate, endDate)
    .ToListAsync(_connection);

// NULL checks
var withDescription = await QueryBuilder.Query<Product>()
    .WhereNotNull(p => p.Description)
    .WhereNull(p => p.DeletedAt)
    .ToListAsync(_connection);

// Complex boolean logic with AND/OR
var complex = await QueryBuilder.Query<Product>()
    .Where(p => p.Category == "Electronics")
    .Or(p => p.Featured == true)
    .And(p => p.InStock == true)
    .ToListAsync(_connection);

Dynamic Query Building for Search/Filters

public async Task<IEnumerable<Product>> SearchProductsAsync(
    string searchTerm,
    decimal? minPrice,
    decimal? maxPrice,
    int[] categoryIds,
    bool? inStockOnly,
    int page = 0,
    int pageSize = 20)
{
    // Build query dynamically based on provided filters
    var query = QueryBuilder.Query<Product>();

    // Add search term if provided
    if (!string.IsNullOrEmpty(searchTerm))
        query.Where("Name LIKE @searchTerm OR Description LIKE @searchTerm",
            new { searchTerm = $"%{searchTerm}%" });

    // Add price range filters
    if (minPrice.HasValue)
        query.Where(p => p.Price >= minPrice.Value);

    if (maxPrice.HasValue)
        query.Where(p => p.Price <= maxPrice.Value);

    // Add category filter
    if (categoryIds?.Any() == true)
        query.WhereIn(p => p.CategoryId, categoryIds);

    // Add stock filter
    if (inStockOnly == true)
        query.Where(p => p.Stock > 0);

    // Apply sorting and pagination
    query.OrderBy(p => p.Name)
         .Skip(page * pageSize)
         .Take(pageSize);

    return await query.ToListAsync(_connection);
}

Joins and Aggregations with QueryBuilder

// INNER JOIN with type-safe expressions
var productsWithCategories = await QueryBuilder.Query<Product>()
    .InnerJoin<Category>((p, c) => p.CategoryId == c.Id)
    .Where(p => p.Price > 50)
    .Select("p.*, c.Name as CategoryName")
    .ToListAsync(_connection);

// LEFT JOIN to include products without categories
var allProductsWithOptionalCategory = await QueryBuilder.Query<Product>()
    .LeftJoin<Category>((p, c) => p.CategoryId == c.Id)
    .OrderBy(p => p.Name)
    .ToListAsync(_connection);

// Multiple JOINs for complex queries
var orderDetails = await QueryBuilder.Query<Order>()
    .InnerJoin<Customer>((o, c) => o.CustomerId == c.Id)
    .InnerJoin<Product>((o, p) => o.ProductId == p.Id)
    .WhereBetween(o => o.OrderDate, DateTime.Today.AddDays(-30), DateTime.Today)
    .Select("o.*, c.Name as CustomerName, p.Name as ProductName")
    .ToListAsync(_connection);

// COUNT aggregation
var activeProductCount = await QueryBuilder.Query<Product>()
    .Where(p => p.Active == true)
    .CountAsync(_connection);

// GROUP BY with aggregations
var categoryStats = await QueryBuilder.Query<Product>()
    .GroupBy(p => p.Category)
    .Having(p => p.Price > 10)
    .Select("Category, COUNT(*) as Count, AVG(Price) as AvgPrice, MAX(Price) as MaxPrice")
    .ToListAsync<CategoryStats>(_connection);

Multi-Step Transaction with Audit Trail

public async Task<Result> ProcessOrderAsync(Order order)
{
    var wrappedConn = _connection
        .WithExceptionTranslation()
        .WithRetryPolicy(_retryPolicy);

    using var transaction = await wrappedConn.BeginTransactionAsync();
    try
    {
        // 1. Insert order
        var orderId = await wrappedConn.InsertAsync(order, transaction);

        // 2. Update inventory
        foreach (var item in order.Items)
        {
            var rowsAffected = await wrappedConn.ExecuteAsync(
                "UPDATE Products SET Stock = Stock - @Quantity WHERE Id = @ProductId AND Stock >= @Quantity",
                new { item.Quantity, item.ProductId }, transaction);

            if (rowsAffected == 0)
                throw new InsufficientStockException(item.ProductId);
        }

        // 3. Create audit log
        await wrappedConn.InsertAsync(new AuditLog
        {
            EntityType = "Order",
            EntityId = orderId,
            Action = "Created",
            UserId = order.UserId,
            Timestamp = DateTime.UtcNow
        }, transaction);

        await transaction.CommitAsync();
        return Result.Success(orderId);
    }
    catch (ReferenceConstraintException ex)
    {
        await transaction.RollbackAsync();
        return Result.Error("Invalid customer or product reference");
    }
    catch (InsufficientStockException ex)
    {
        await transaction.RollbackAsync();
        return Result.Error($"Insufficient stock for product {ex.ProductId}");
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

Performance Optimization Patterns

// Combine entity extensions with QueryBuilder for optimal performance
public async Task<DashboardData> GetDashboardDataAsync(int userId)
{
    // 1. Get user by ID (strongly-typed)
    var user = await _connection.GetAsync<User>(userId);

    // 2. Get recent orders with QueryBuilder (filtered and paginated)
    var recentOrders = await QueryBuilder.Query<Order>()
        .Where(o => o.UserId == userId)
        .OrderByDescending(o => o.OrderDate)
        .Take(10)
        .ToListAsync(_connection);

    // 3. Get order count
    var totalOrders = await QueryBuilder.Query<Order>()
        .Where(o => o.UserId == userId)
        .CountAsync(_connection);

    // 4. Calculate total revenue with aggregation query
    var totalRevenue = await _connection.ExecuteScalarAsync<decimal?>(
        @"SELECT SUM(TotalAmount)
          FROM Orders
          WHERE UserId = @UserId AND Status = @Status",
        new { UserId = userId, Status = "Completed" }) ?? 0;

    return new DashboardData
    {
        User = user,
        RecentOrders = recentOrders,
        TotalOrders = totalOrders,
        TotalRevenue = totalRevenue
    };
}

// Efficient pagination with total count
public async Task<PagedResult<Product>> GetPagedProductsAsync(
    string category,
    int page,
    int pageSize)
{
    // Build base query
    var query = QueryBuilder.Query<Product>()
        .Where(p => p.Category == category)
        .Where(p => p.Active == true);

    // Get total count (before pagination)
    var totalCount = await query.CountAsync(_connection);

    // Get paginated results
    var products = await query
        .OrderBy(p => p.Name)
        .Skip(page * pageSize)
        .Take(pageSize)
        .ToListAsync(_connection);

    return new PagedResult<Product>
    {
        Items = products,
        TotalCount = totalCount,
        Page = page,
        PageSize = pageSize
    };
}

Noundry.TagHelpers

ASP.NET Core TagHelpers optimized for Tailwind CSS with built-in accessibility and responsive design.

Installation

$ dotnet add package Noundry.TagHelpers

Form TagHelpers

<!-- Enhanced form with validation -->
<noundry-form asp-action="Create" class="space-y-6">
    <noundry-input asp-for="Name" label="Full Name" required/>
    <noundry-input asp-for="Email" type="email" label="Email Address"/>
    <noundry-select asp-for="Country" asp-items="ViewBag.Countries" label="Country"/>
    <noundry-button type="submit" variant="primary">Submit</noundry-button>
</noundry-form>

Noundry.Authnz

Sessionless OAuth 2.0 authentication for multiple providers with JWT token management. Secure, scalable, and easy to integrate into any .NET application.

Key Features

Multiple Providers

  • • Google OAuth 2.0
  • • Microsoft Azure AD
  • • GitHub OAuth
  • • Apple Sign In
  • • Facebook & Twitter
  • • Custom providers

Security Features

  • • Sessionless architecture with JWT
  • • PKCE support for enhanced security
  • • Automatic token refresh
  • • CSRF protection
  • • State parameter validation
  • • Secure cookie management

Installation

$ dotnet add package Noundry.Authnz

Quick Setup

// Program.cs
using Noundry.Authnz.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add OAuth authentication
builder.Services.AddNoundryOAuth(options =>
{
    options.AddGoogle(
        builder.Configuration["GOOGLE_CLIENT_ID"],
        builder.Configuration["GOOGLE_CLIENT_SECRET"]
    );
    options.AddMicrosoft(
        builder.Configuration["MICROSOFT_CLIENT_ID"],
        builder.Configuration["MICROSOFT_CLIENT_SECRET"]
    );
    options.AddGitHub(
        builder.Configuration["GITHUB_CLIENT_ID"],
        builder.Configuration["GITHUB_CLIENT_SECRET"]
    );
    
    options.JwtSecret = builder.Configuration["JWT_SECRET"];
    options.DefaultRedirectUri = "/Dashboard";
});

var app = builder.Build();

// Configure authentication pipeline
app.UseNoundryOAuth();
app.UseAuthentication();
app.UseAuthorization();

app.Run();

TagHelper Components

Login Component

<!-- Show all providers -->
<noundry-oauth-login show-all="true"></noundry-oauth-login>

<!-- Individual provider -->
<noundry-oauth-login provider="google"></noundry-oauth-login>

<!-- Custom styling -->
<noundry-oauth-login 
    provider="microsoft"
    button-text="Sign in with Microsoft"
    button-class="custom-btn-style">
</noundry-oauth-login>

User Status

<!-- Anonymous users -->
<noundry-oauth-status 
    show-when-anonymous="true">
    <a asp-page="/Auth/Login">Sign In</a>
</noundry-oauth-status>

<!-- Authenticated users -->
<noundry-oauth-status 
    show-when-authenticated="true"
    show-avatar="true"
    show-name="true"
    show-email="true">
    <a asp-page="/Auth/Logout">Logout</a>
</noundry-oauth-status>

API Protection

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Protected endpoint - authentication required
    [HttpPost]
    [Authorize]
    public IActionResult CreateProduct(Product product)
    {
        // Access user information from JWT claims
        var userId = User.FindFirst("sub")?.Value;
        var email = User.FindFirst("email")?.Value;
        
        product.CreatedBy = userId;
        return Ok(CreateProduct(product));
    }
    
    // Role-based authorization
    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")]
    public IActionResult DeleteProduct(int id)
    {
        return Ok(DeleteProduct(id));
    }
}

Real-World Authentication Flows

Production-ready authentication patterns for secure applications:

Complete OAuth Flow with Error Handling

// Pages/Auth/Login.cshtml.cs
public class LoginModel : PageModel
{
    private readonly IOAuthService _oauthService;
    private readonly ILogger<LoginModel> _logger;

    public LoginModel(IOAuthService oauthService, ILogger<LoginModel> logger)
    {
        _oauthService = oauthService;
        _logger = logger;
    }

    public IActionResult OnGetAsync(string provider, string returnUrl = null)
    {
        try
        {
            // Generate secure state and PKCE parameters
            var authRequest = _oauthService.CreateAuthorizationRequest(
                provider,
                returnUrl ?? "/Dashboard"
            );

            // Store state in secure cookie for validation
            Response.Cookies.Append("oauth_state", authRequest.State, new CookieOptions
            {
                HttpOnly = true,
                Secure = true,
                SameSite = SameSiteMode.Lax,
                MaxAge = TimeSpan.FromMinutes(10)
            });

            return Redirect(authRequest.AuthorizationUrl);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "OAuth authorization request failed for provider: {Provider}", provider);
            TempData["Error"] = "Authentication initialization failed";
            return RedirectToPage("/Error");
        }
    }

    public async Task<IActionResult> OnGetCallbackAsync(string code, string state)
    {
        try
        {
            // Validate state to prevent CSRF
            var savedState = Request.Cookies["oauth_state"];
            if (savedState != state)
            {
                _logger.LogWarning("OAuth state mismatch - possible CSRF attack");
                return RedirectToPage("/Error");
            }

            // Exchange code for tokens
            var tokenResponse = await _oauthService.ExchangeCodeForTokensAsync(code, state);

            // Create user session with JWT
            await SignInUserAsync(tokenResponse);

            // Clean up state cookie
            Response.Cookies.Delete("oauth_state");

            return Redirect(tokenResponse.ReturnUrl ?? "/Dashboard");
        }
        catch (OAuthException ex)
        {
            _logger.LogError(ex, "OAuth callback failed: {Error}", ex.Message);
            TempData["Error"] = "Authentication failed. Please try again.";
            return RedirectToPage("/Auth/Login");
        }
    }
}

JWT Token Management with Auto-Refresh

// Middleware/TokenRefreshMiddleware.cs
public class TokenRefreshMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<TokenRefreshMiddleware> _logger;

    public async Task InvokeAsync(
        HttpContext context,
        ITokenService tokenService)
    {
        if (context.User.Identity?.IsAuthenticated == true)
        {
            // Check token expiration
            var expClaim = context.User.FindFirst("exp")?.Value;
            if (long.TryParse(expClaim, out var exp))
            {
                var expiryTime = DateTimeOffset.FromUnixTimeSeconds(exp);
                var timeUntilExpiry = expiryTime - DateTimeOffset.UtcNow;

                // Refresh if within 5 minutes of expiry
                if (timeUntilExpiry.TotalMinutes < 5)
                {
                    try
                    {
                        var refreshToken = context.Request.Cookies["refresh_token"];
                        if (!string.IsNullOrEmpty(refreshToken))
                        {
                            var newTokens = await tokenService.RefreshTokensAsync(refreshToken);

                            // Update access token cookie
                            context.Response.Cookies.Append("access_token", newTokens.AccessToken,
                                new CookieOptions
                                {
                                    HttpOnly = true,
                                    Secure = true,
                                    SameSite = SameSiteMode.Strict,
                                    MaxAge = TimeSpan.FromHours(1)
                                });

                            _logger.LogInformation("Access token refreshed for user: {UserId}",
                                context.User.FindFirst("sub")?.Value);
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Token refresh failed");
                        // Allow request to continue - user will need to re-authenticate
                    }
                }
            }
        }

        await _next(context);
    }
}

Secure API Client Authentication

// Services/AuthenticatedApiClient.cs
public class AuthenticatedApiClient
{
    private readonly HttpClient _httpClient;
    private readonly ITokenService _tokenService;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public async Task<T> GetAsync<T>(string url)
    {
        var token = await GetValidTokenAsync();
        _httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        var response = await _httpClient.GetAsync(url);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            // Attempt token refresh on 401
            var refreshedToken = await RefreshAndGetTokenAsync();
            if (refreshedToken != null)
            {
                _httpClient.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", refreshedToken);
                response = await _httpClient.GetAsync(url);
            }
        }

        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<T>(content);
    }

    private async Task<string> GetValidTokenAsync()
    {
        var context = _httpContextAccessor.HttpContext;
        var token = context?.Request.Cookies["access_token"];

        // Validate token expiration
        if (_tokenService.IsTokenExpired(token))
        {
            return await RefreshAndGetTokenAsync();
        }

        return token;
    }

    private async Task<string> RefreshAndGetTokenAsync()
    {
        var context = _httpContextAccessor.HttpContext;
        var refreshToken = context?.Request.Cookies["refresh_token"];

        if (string.IsNullOrEmpty(refreshToken))
            throw new UnauthorizedException("No refresh token available");

        var newTokens = await _tokenService.RefreshTokensAsync(refreshToken);

        // Update cookies
        context.Response.Cookies.Append("access_token", newTokens.AccessToken,
            new CookieOptions { HttpOnly = true, Secure = true });

        return newTokens.AccessToken;
    }
}

Advanced Patterns

Production-ready patterns for enterprise applications:

Repository Pattern with IDbConnection

using System.Data;
using Noundry.Tuxedo.Patterns;

public class ProductService
{
    private readonly TuxedoRepository<Product> _productRepo;
    private readonly IDbConnection _connection;

    public ProductService(IDbConnection connection)
    {
        _connection = connection;
        _productRepo = new TuxedoRepository<Product>(connection);
    }

    // Find with LINQ expressions
    public async Task<IEnumerable<Product>> GetLowStockProductsAsync(int threshold)
    {
        return await _productRepo.FindAsync(p => p.Stock < threshold && !p.Discontinued);
    }

    // Count with predicate
    public async Task<int> GetActiveProductCountAsync()
    {
        return await _productRepo.CountAsync(p => p.IsActive);
    }

    // Paginated results
    public async Task<PagedResult<Product>> GetProductsPagedAsync(int page, int size)
    {
        return await _productRepo.GetPagedAsync(page, size, p => p.IsActive);
    }
}

QueryBuilder with IDbConnection

using System.Data;
using Noundry.Tuxedo.QueryBuilder;
using Noundry.Tuxedo.DependencyInjection;

public class ProductSearchService
{
    private readonly IDbConnection _connection;

    public ProductSearchService(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<IEnumerable<Product>> SearchAsync(SearchCriteria criteria)
    {
        var builder = new QueryBuilder<Product>(TuxedoDialect.SqlServer);

        builder.SelectAll();

        if (!string.IsNullOrEmpty(criteria.Category))
            builder.Where(p => p.Category == criteria.Category);

        if (criteria.MinPrice.HasValue && criteria.MaxPrice.HasValue)
            builder.WhereBetween(p => p.Price, criteria.MinPrice.Value, criteria.MaxPrice.Value);

        if (criteria.Tags?.Any() == true)
            builder.WhereIn(p => p.TagId, criteria.Tags);

        builder.OrderBy(p => p.Name);

        if (criteria.PageSize > 0)
        {
            builder.Skip(criteria.Page * criteria.PageSize);
            builder.Take(criteria.PageSize);
        }

        return await builder.ToListAsync(_connection);
    }
}

Unit of Work Pattern with Transactions

using System.Data;
using Noundry.Tuxedo.Patterns;

public class OrderService
{
    private readonly IDbConnection _connection;

    public OrderService(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        using var uow = new UnitOfWork(_connection);

        // Create repositories within the same transaction
        var orderRepo = uow.GetRepository<Order>();
        var productRepo = uow.GetRepository<Product>();
        var inventoryRepo = uow.GetRepository<InventoryLog>();

        try
        {
            // 1. Create order
            await orderRepo.AddAsync(order);

            // 2. Reduce inventory for each item
            foreach (var item in order.Items)
            {
                var product = await productRepo.GetByIdAsync(item.ProductId);

                if (product.Stock < item.Quantity)
                    throw new InvalidOperationException($"Insufficient stock for {product.Name}");

                product.Stock -= item.Quantity;
                await productRepo.UpdateAsync(product);

                // 3. Log inventory change
                await inventoryRepo.AddAsync(new InventoryLog
                {
                    ProductId = product.Id,
                    Change = -item.Quantity,
                    Reason = $"Order {order.Id}",
                    Timestamp = DateTime.UtcNow
                });
            }

            // Commit all changes atomically
            await uow.CommitAsync();
            return order;
        }
        catch
        {
            await uow.RollbackAsync();
            throw;
        }
    }
}

Direct Queries with IDbConnection

using System.Data;
using Noundry.Tuxedo;

public class ReportService
{
    private readonly IDbConnection _connection;

    public ReportService(IDbConnection connection)
    {
        _connection = connection;
    }

    // Complex aggregation query
    public async Task<IEnumerable<SalesReport>> GetMonthlySalesAsync(int year)
    {
        var sql = @"
            SELECT
                MONTH(OrderDate) AS Month,
                COUNT(*) AS OrderCount,
                SUM(TotalAmount) AS TotalSales,
                AVG(TotalAmount) AS AverageSale
            FROM Orders
            WHERE YEAR(OrderDate) = @Year
            GROUP BY MONTH(OrderDate)
            ORDER BY Month";

        return await _connection.QueryAsync<SalesReport>(sql, new { Year = year });
    }

    // Multi-mapping for complex objects
    public async Task<IEnumerable<OrderWithDetails>> GetOrdersWithCustomerAsync()
    {
        var sql = @"
            SELECT o.*, c.*
            FROM Orders o
            INNER JOIN Customers c ON o.CustomerId = c.Id
            WHERE o.Status = @Status";

        var orders = await _connection.QueryAsync<Order, Customer, OrderWithDetails>(
            sql,
            (order, customer) => new OrderWithDetails
            {
                Order = order,
                Customer = customer
            },
            new { Status = "Active" },
            splitOn: "Id"
        );

        return orders;
    }

    // Stored procedure execution
    public async Task<int> ArchiveOldOrdersAsync(int daysOld)
    {
        return await _connection.ExecuteAsync(
            "sp_ArchiveOrders",
            new { DaysOld = daysOld },
            commandType: CommandType.StoredProcedure
        );
    }
}

Automatic Audit Logging

Comprehensive change tracking for Insert, Update, and Delete operations with automatic audit trail generation:

Basic Usage

using Noundry.Tuxedo.Auditor;
using Noundry.Tuxedo.Auditor.UserProviders;

// Wrap connection with automatic auditing
var auditedConn = connection.WithAuditing(config =>
{
    config.UserProvider = new StaticAuditUserProvider("admin");
    config.AuditTableName = "audit_log"; // Optional, default
});

// All CRUD operations are automatically audited
await auditedConn.InsertAsync(product);  // Logged: Insert with new values
await auditedConn.UpdateAsync(product);  // Logged: Update with old/new values + changed columns
await auditedConn.DeleteAsync(product);  // Logged: Delete with old values

User Providers

// Static user (testing, background jobs)
config.UserProvider = new StaticAuditUserProvider("system");

// Delegate provider (custom logic)
config.UserProvider = new DelegateAuditUserProvider(() =>
{
    return Thread.CurrentPrincipal?.Identity?.Name ?? "Anonymous";
});

// HTTP Context (ASP.NET Core web apps)
config.UserProvider = new HttpContextAuditUserProvider(httpContextAccessor);

ASP.NET Core Dependency Injection

// In Program.cs or Startup.cs
services.AddHttpContextAccessor();

services.AddScoped<IDbConnection>(sp =>
{
    var httpContext = sp.GetRequiredService<IHttpContextAccessor>();
    var connectionString = sp.GetRequiredService<IConfiguration>()
        .GetConnectionString("DefaultConnection");

    var connection = new SqlConnection(connectionString);

    return connection.WithAuditing(config =>
    {
        config.UserProvider = new HttpContextAuditUserProvider(httpContext);
        config.ExcludeTables("Logs", "TempData");
    });
});

Automatic Soft Delete Detection

public class User
{
    [Key]
    public int Id { get; set; }
    public string Email { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

// Regular update
user.Email = "newemail@example.com";
await auditedConn.UpdateAsync(user);
// Event Type: "Update"

// Soft delete (automatically detected)
user.IsDeleted = true;
user.DeletedAt = DateTime.UtcNow;
await auditedConn.UpdateAsync(user);
// Event Type: "SoftDelete"

Combine with Exceptions and Resiliency

using Noundry.Tuxedo.Auditor;
using Noundry.Tuxedo.Exceptions;
using Noundry.Tuxedo.Resiliency;

// Combine all three features (order doesn't matter!)
var connection = new SqlConnection(connectionString)
    .WithAuditing(config => config.UserProvider = new StaticAuditUserProvider("admin"))
    .WithExceptionTranslation()
    .WithRetryPolicy(retryPolicy);

Querying Audit Logs

// Get audit history for a specific entity
var history = await connection.QueryAsync<AuditEvent>(@"
    SELECT * FROM audit_log
    WHERE table_name = @Table AND entity_id = @EntityId
    ORDER BY timestamp DESC",
    new { Table = "Users", EntityId = "123" }
);

// Get all changes by a specific user
var userChanges = await connection.QueryAsync<AuditEvent>(
    "SELECT * FROM audit_log WHERE username = @Username ORDER BY timestamp DESC",
    new { Username = "admin" }
);

// Get today's changes
var todaysChanges = await connection.QueryAsync<AuditEvent>(@"
    SELECT * FROM audit_log
    WHERE timestamp >= @StartOfDay
    ORDER BY timestamp DESC",
    new { StartOfDay = DateTime.Today }
);

Install: dotnet add package Noundry.Tuxedo.Auditor

Features: Automatic Insert/Update/Delete logging, old/new value tracking, changed column detection, soft delete detection, user tracking

Audit Table: Auto-created with event type, table name, entity ID, username, timestamp, old/new values (JSON), changed columns

Databases: SQL Server, PostgreSQL (JSONB), MySQL (JSON), SQLite (TEXT)

Noundry.Tuxedo.Bowtie

Automated database schema synchronization library that creates and updates database tables directly from your C# model classes. No manual migrations needed.

Key Features

Database Support

  • • SQL Server
  • • PostgreSQL
  • • MySQL
  • • SQLite

Advanced Features

  • • Model-first schema generation
  • • Advanced indexing (B-Tree, Hash, GIN)
  • • Complex constraints support
  • • CLI and programmatic APIs

Installation

$ dotnet add package Noundry.Tuxedo.Bowtie

Model Definition

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Noundry.Tuxedo.Bowtie.Attributes;

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

    [Column(MaxLength = 100)]
    [Index("IX_Products_Name")]
    public string Name { get; set; } = string.Empty;

    [Column(Precision = 18, Scale = 2)]
    [CheckConstraint("Price > 0")]
    public decimal Price { get; set; }

    [Column(MaxLength = 500)]
    public string? Description { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }

    [Column(DefaultValue = "GETDATE()")]
    public DateTime CreatedAt { get; set; }
}

Database Synchronization

// Program.cs - Automatic synchronization on startup
using Noundry.Tuxedo.Bowtie;

var builder = WebApplication.CreateBuilder(args);

// Add Bowtie services
builder.Services.AddBowtie(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
    options.Provider = DatabaseProvider.SQLServer;
    options.AutoMigrate = true; // Automatically sync on startup
});

var app = builder.Build();

// Manual synchronization
await app.Services.SynchronizeDatabaseAsync(
    connectionString: "Data Source=app.db",
    provider: DatabaseProvider.SQLite
);

Advanced Indexing & Constraints

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

    // Unique constraint with custom index
    [Column(MaxLength = 255)]
    [Index("IX_Users_Email", IsUnique = true)]
    public string Email { get; set; } = string.Empty;

    // Full-text search index (PostgreSQL)
    [Column(MaxLength = 1000)]
    [Index("IX_Users_Bio_GIN", IndexType = IndexType.GIN)]
    public string? Biography { get; set; }

    // Composite index
    [Index("IX_Users_Name_Status", Order = 1)]
    public string FirstName { get; set; } = string.Empty;

    [Index("IX_Users_Name_Status", Order = 2)]
    [CheckConstraint("Status IN ('Active', 'Inactive', 'Pending')")]
    public string Status { get; set; } = "Pending";
}

CLI Usage

# Install CLI tool
$ dotnet tool install -g Noundry.Tuxedo.Bowtie.Cli

# Generate schema from models
$ bowtie sync --connection "Server=localhost;Database=MyApp;" --provider sqlserver

# Dry run to see what changes would be made
$ bowtie sync --connection "Data Source=app.db" --provider sqlite --dry-run

# Generate DDL script without applying
$ bowtie script --connection "Host=localhost;Database=myapp" --provider postgresql --output schema.sql

# Validate models against database
$ bowtie validate --connection "Server=localhost;Database=MyApp;" --provider sqlserver

Noundry.Tuxedo.Cufflink

Intelligent fake data generation library and CLI tool for Tuxedo ORM. Generate realistic test data with automatic foreign key and primary key relationship detection.

Installation

$ dotnet add package Noundry.Tuxedo.Cufflink
# Or install CLI tool
$ dotnet tool install -g Noundry.Tuxedo.Cufflink.CLI

Quick Start

using Noundry.Tuxedo.Cufflink;

// Generate fake data for your models
var generator = new CufflinkGenerator();
var customers = generator.Generate<Customer>(100);

// Automatically handles FK relationships
generator.GenerateAndInsert<Order>(500);

CLI Usage

# Generate data for specific table
$ cufflink generate --table Products --rows 100 \
    --connection "Server=localhost;Database=TestDb"

# Generate for entire database
$ cufflink generate-all --rows-per-table 50 \
    --connection "Server=localhost;Database=TestDb"

Key Features

  • • Automatic FK/PK relationship detection
  • • 100+ realistic data generators (names, emails, addresses, etc.)
  • • CLI tool and library modes
  • • Customizable data generation rules

Noundry.Slurp

High-performance CSV to database ingestion tool capable of processing millions of rows in seconds with zero-allocation parsing and intelligent schema inference.

Key Features

⚡ Blazing Performance

  • • 100,000+ rows per second typical performance
  • • Zero-allocation CSV parsing using SEP library
  • • Minimal memory footprint
  • • Parallel processing capabilities

🗄️ Multi-Database Support

  • • SQL Server
  • • PostgreSQL
  • • MySQL
  • • SQLite

Installation

$ dotnet add package Noundry.Slurp

Usage Examples

Interactive Mode

# Launch interactive CLI with beautiful prompts
$ slurp data.csv

# The CLI will guide you through:
# - Database provider selection
# - Connection configuration
# - Table name and schema options

Command Line Mode

# Direct command with all parameters
$ slurp data.csv -p postgres -s localhost -d mydb -u myuser --password mypass

# SQL Server example
$ slurp sales.csv -p sqlserver -s localhost -d Sales -u sa --password MyPass123!

# SQLite example (simple local database)
$ slurp inventory.csv -p sqlite -d inventory.db

Library Usage

using Noundry.Slurp;

// Configure the ingestion engine
var config = new SlurpConfiguration
{
    Provider = "postgres",
    Server = "localhost",
    Database = "mydb",
    Username = "user",
    Password = "pass"
};

// Create and run the engine
var engine = new SlurpEngine(config);
var result = await engine.IngestAsync("data.csv");

// Check results
Console.WriteLine($"Ingested {result.RowCount} rows in {result.ElapsedTime}");

Advanced Features

Smart Schema Inference

Automatically detects column types, sizes, and constraints from your CSV data, creating optimal database schemas without manual configuration.

Intelligent Indexing

Automatically creates indexes on commonly queried columns like ID fields and foreign keys for optimal query performance.

CSV Configuration

Supports various CSV formats with configurable delimiters, quotes, escape characters, and encoding options.

Noundry.Connector

Powerful API connector library for .NET that provides strongly-typed HTTP clients with automatic authentication, LINQ querying, and comprehensive code generation capabilities.

Key Features

🚀 CLI Code Generator

Interactive wizard to generate strongly-typed C# models and Refit interfaces from REST APIs

🔐 Automatic Authentication

Built-in authentication with support for Bearer tokens, API keys, and OAuth 2.0

🎯 Type-Safe API Clients

Compile-time safety with strongly-typed models and enhanced IntelliSense

🔍 Advanced LINQ Support

Query API responses like in-memory collections with full LINQ integration

Installation

CLI Tool

$ dotnet tool install -g Noundry.Connector.Generator

Core Library

$ dotnet add package Noundry.Connector

Quick Start with CLI Generator

Generate JSONPlaceholder Client

# Start the interactive generator
$ noundry-connector generate

# Or use command line options:
$ noundry-connector generate \
  --url "https://jsonplaceholder.typicode.com" \
  --client-name "JsonPlaceholderClient" \
  --namespace "MyApp.ApiClients"

Register and Use Generated Client

// Register in DI
builder.Services.AddJsonPlaceholderClient("https://jsonplaceholder.typicode.com");

// Inject and use
public async Task<IEnumerable<Post>> GetRecentPostsAsync()
{
    var posts = await _client.GetAllPostsAsync();
    return posts.Where(p => p.UserId <= 5)
                    .OrderByDescending(p => p.Id)
                    .Take(10);
}

Manual Setup (Without CLI)

Define Models and Interfaces

// 1. Define your model
public class User
{
    [JsonPropertyName("id")]
    public int Id { get; set; }

    [JsonPropertyName("email")]
    public string Email { get; set; }
}

// 2. Define Refit interface
public interface IUserApi
{
    [Get("/users")]
    Task<IEnumerable<User>> GetUsersAsync();

    [Post("/users")]
    Task<User> CreateUserAsync([Body] User user);
}

// 3. Register in DI
services.AddConnector<IUserApi>(options =>
{
    options.BaseUrl = "https://api.example.com";
});

Authentication

Bearer Token

services.AddTokenAuthentication("your-api-token");
services.AddConnector<IYourApi>(opt => opt.BaseUrl = "https://api.example.com");

OAuth 2.0

services.AddOAuthAuthentication(config =>
{
    config.ClientId = configuration["GitHub:ClientId"];
    config.ClientSecret = configuration["GitHub:ClientSecret"];
    config.TokenEndpoint = "https://github.com/login/oauth/access_token";
});

Advanced LINQ Querying

Complex Filtering

var orders = await orderClient.GetOrdersAsync();

var vipCustomers = orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new {
        CustomerId = g.Key,
        TotalSpent = g.Sum(o => o.TotalAmount),
        OrderCount = g.Count()
    })
    .Where(c => c.TotalSpent > 10000)
    .OrderByDescending(c => c.TotalSpent);

Real-World Example: E-Commerce Recommendations

public async Task<List<Product>> GetRecommendationsAsync(int customerId)
{
    var orders = await _orderApi.GetCustomerOrdersAsync(customerId);
    var products = await _productApi.GetProductsAsync();

    var preferredCategories = orders
        .SelectMany(o => o.Items)
        .Join(products, item => item.ProductId, p => p.Id, (i, p) => p.CategoryId)
        .GroupBy(cat => cat)
        .OrderByDescending(g => g.Count())
        .Take(3);

    return products
        .Where(p => preferredCategories.Contains(p.CategoryId))
        .OrderByDescending(p => p.Rating)
        .Take(10)
        .ToList();
}

Noundry.DotEnvX

Enhanced .env file support with encryption, validation, and environment-specific configurations. Secure your application secrets while maintaining developer productivity.

Key Features

Security

  • • AES-256 encryption for sensitive values
  • • Secure key derivation (PBKDF2)
  • • Automatic encryption detection
  • • Safe default configurations

Developer Experience

  • • Environment-specific files
  • • Required variable validation
  • • Auto-loading and hot-reload
  • • Integration with ASP.NET Configuration

Installation

$ dotnet add package Noundry.DotEnvX

Basic Usage

// Program.cs
using Noundry.DotEnvX.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add DotEnvX configuration
builder.Configuration.AddDotEnvX(options =>
{
    options.Path = ".env";
    options.EnvironmentSpecific = true; // Loads .env.development, .env.production, etc.
    options.Required = new[] { "DATABASE_URL", "JWT_SECRET" };
    options.EncryptionKey = "your-encryption-key"; // For encrypted values
    options.ThrowOnMissingRequired = true;
});

Environment Files

.env (Base)

# Base configuration
APP_NAME=MyApplication
LOG_LEVEL=Information

# Database
DATABASE_HOST=localhost
DATABASE_PORT=5432

# Encrypted secrets
JWT_SECRET=encrypted:BDb7t3QkTRp2AbCdEfGhIjKl...
API_KEY=encrypted:XyZ9w8v7u6t5s4r3q2p1o0n...

.env.development

# Development overrides
LOG_LEVEL=Debug
DATABASE_URL=postgres://user:pass@localhost/dev_db

# Development-specific settings
ENABLE_SWAGGER=true
CORS_ORIGINS=http://localhost:3000,http://localhost:5173

# Non-encrypted for development
JWT_SECRET=development-secret-key

Encryption & Security

// Encrypt values programmatically
var encryptor = new DotEnvXEncryptor("your-encryption-key");
var encryptedValue = encryptor.Encrypt("sensitive-data");
Console.WriteLine($"encrypted:{encryptedValue}");

// Or use CLI tool
// $ dotenvx encrypt "my-secret-value"
// encrypted:BDb7t3QkTRp2AbCdEfGhIjKlMnOpQrStUv...

// Values are automatically decrypted when loaded
var jwtSecret = builder.Configuration["JWT_SECRET"]; // Returns decrypted value

Advanced Configuration

builder.Configuration.AddDotEnvX(options =>
{
    // Multiple environment files
    options.Files = new[] 
    {
        ".env",
        ".env.local",
        $".env.{builder.Environment.EnvironmentName.ToLower()}"
    };
    
    // Validation rules
    options.Required = new[] 
    { 
        "DATABASE_URL", 
        "JWT_SECRET", 
        "ENCRYPTION_KEY" 
    };
    
    // Custom validation
    options.Validators.Add("JWT_SECRET", value => 
        value.Length >= 32 ? null : "JWT_SECRET must be at least 32 characters");
    
    // Environment overrides
    options.AllowSystemEnvironmentOverrides = true;
    options.OverrideExistingVariables = false;
    
    // Security settings
    options.EncryptionKey = Environment.GetEnvironmentVariable("DOTENVX_KEY");
    options.RequireEncryption = builder.Environment.IsProduction();
});

Real-World Encryption Scenarios

Production-ready patterns for secure secret management:

CI/CD Pipeline Secret Management

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      # Store encryption key in GitHub Secrets
      - name: Setup environment
        env:
          DOTENVX_KEY: ${{ secrets.DOTENVX_ENCRYPTION_KEY }}
        run: |
          # Encrypt production secrets
          dotnet tool install -g Noundry.DotEnvX.Cli
          dotenvx encrypt --file .env.production --key $DOTENVX_KEY

          # Verify encrypted values
          dotenvx validate --file .env.production

      - name: Deploy application
        run: |
          dotnet publish -c Release
          dotnet deploy --environment production

Multi-Environment Encryption Strategy

// Services/EnvironmentSecretManager.cs
public class EnvironmentSecretManager
{
    private readonly IConfiguration _configuration;
    private readonly IWebHostEnvironment _environment;

    public EnvironmentSecretManager(
        IConfiguration configuration,
        IWebHostEnvironment environment)
    {
        _configuration = configuration;
        _environment = environment;
    }

    public string GetSecret(string key)
    {
        // Development: Use plain text secrets from .env.development
        if (_environment.IsDevelopment())
        {
            return _configuration[key] ?? throw new InvalidOperationException(
                $"Secret '{key}' not found in .env.development");
        }

        // Staging: Use encrypted secrets from .env.staging
        if (_environment.IsStaging())
        {
            var stagingValue = _configuration[$"STAGING_{key}"] ?? _configuration[key];
            return stagingValue ?? throw new InvalidOperationException(
                $"Secret '{key}' not found in .env.staging");
        }

        // Production: Use encrypted secrets with fallback to system environment
        if (_environment.IsProduction())
        {
            var prodValue = _configuration[$"PROD_{key}"]
                          ?? _configuration[key]
                          ?? Environment.GetEnvironmentVariable(key);

            if (string.IsNullOrEmpty(prodValue))
            {
                // Log security event
                throw new SecurityException(
                    $"Required production secret '{key}' not found. Application cannot start.");
            }

            return prodValue;
        }

        throw new InvalidOperationException("Unknown environment");
    }
}

// Program.cs - Setup
builder.Configuration.AddDotEnvX(options =>
{
    // Load environment-specific file
    var env = builder.Environment.EnvironmentName.ToLower();
    options.Files = new[] { ".env", $".env.{env}" };

    // Get encryption key from system environment (set by deployment platform)
    options.EncryptionKey = Environment.GetEnvironmentVariable("DOTENVX_KEY");

    // Require encryption in production
    options.RequireEncryption = builder.Environment.IsProduction();
});

Encryption Key Rotation

// Services/KeyRotationService.cs
public class KeyRotationService
{
    private readonly ILogger<KeyRotationService> _logger;

    public async Task RotateEncryptionKeyAsync(
        string envFilePath,
        string oldKey,
        string newKey)
    {
        _logger.LogInformation("Starting encryption key rotation for {FilePath}", envFilePath);

        try
        {
            // 1. Read encrypted file with old key
            var oldEncryptor = new DotEnvXEncryptor(oldKey);
            var envVariables = await File.ReadAllLinesAsync(envFilePath);

            // 2. Decrypt all encrypted values
            var decryptedValues = new Dictionary<string, string>();
            foreach (var line in envVariables)
            {
                if (line.Contains("encrypted:"))
                {
                    var parts = line.Split('=', 2);
                    var key = parts[0].Trim();
                    var encryptedValue = parts[1].Replace("encrypted:", "").Trim();

                    var decrypted = oldEncryptor.Decrypt(encryptedValue);
                    decryptedValues[key] = decrypted;
                }
            }

            // 3. Re-encrypt with new key
            var newEncryptor = new DotEnvXEncryptor(newKey);
            var newLines = new List<string>();

            foreach (var line in envVariables)
            {
                if (line.Contains("encrypted:"))
                {
                    var key = line.Split('=')[0].Trim();
                    var reencrypted = newEncryptor.Encrypt(decryptedValues[key]);
                    newLines.Add($"{key}=encrypted:{reencrypted}");
                }
                else
                {
                    newLines.Add(line);
                }
            }

            // 4. Backup old file
            var backupPath = $"{envFilePath}.backup.{DateTime.UtcNow:yyyyMMddHHmmss}";
            File.Copy(envFilePath, backupPath);

            // 5. Write new encrypted file
            await File.WriteAllLinesAsync(envFilePath, newLines);

            _logger.LogInformation("Key rotation completed successfully. Backup saved to {BackupPath}", backupPath);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Key rotation failed");
            throw;
        }
    }
}

Secure Local Development Workflow

# .env.example (checked into git - no secrets)
APP_NAME=MyApp
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=myapp_dev

# Placeholders for required secrets
DATABASE_PASSWORD=<your-local-db-password>
JWT_SECRET=<generate-32-char-secret>
OAUTH_CLIENT_SECRET=<get-from-google-console>

# .env.local (NOT checked into git - actual secrets)
DATABASE_PASSWORD=my-local-password-123
JWT_SECRET=abcdef1234567890abcdef1234567890
OAUTH_CLIENT_SECRET=actual-oauth-secret-from-google

# .gitignore
.env.local
.env.*.local
.env.production

# Setup script for new developers
# scripts/setup-env.sh
#!/bin/bash

if [ ! -f .env.local ]; then
    echo "Creating .env.local from .env.example..."
    cp .env.example .env.local

    # Generate secure JWT secret
    JWT_SECRET=$(openssl rand -base64 32)
    sed -i "s/<generate-32-char-secret>/$JWT_SECRET/" .env.local

    echo "✅ .env.local created successfully"
    echo "⚠️  Please update remaining placeholders in .env.local"
else
    echo ".env.local already exists"
fi

Noundry Cloud CLI (NDC)

A unified .NET CLI tool providing "write once, deploy anywhere" experience across multiple cloud platforms. Use .NET Aspire for local development and deploy seamlessly to AWS, Google Cloud, Azure, or containers.

Key Features

🌐 Multi-Cloud Support

  • • AWS (App Runner, RDS, ElastiCache)
  • • Google Cloud (Cloud Run, Cloud SQL, Memorystore)
  • • Azure (Container Apps, SQL Database, Redis)
  • • Docker/Kubernetes containers

🏠 Consistent Development

  • • .NET Aspire for local orchestration
  • • Same commands across all platforms
  • • Identical developer experience
  • • Infrastructure as Code with Terraform

Installation

$ dotnet tool install -g noundry-cloud-cli

Prerequisites:

  • • .NET 9.0 SDK
  • • Docker Desktop
  • • Terraform ≥ 1.0
  • • Platform CLI tools (AWS CLI, gcloud, Azure CLI)

Quick Start

# Create a new project
$ ndc new myapp --template webapi
$ cd myapp

# Start local development with Aspire
$ ndc dev
# This starts PostgreSQL, Redis, MinIO locally

# Deploy to any cloud platform
$ ndc deploy --target aws
$ ndc deploy --target gcp
$ ndc deploy --target azure

# Same commands work for all platforms!

Local Development with Aspire

Project Structure

myapp/
├── src/
│   ├── MyApp.Api/          # Your API project
│   └── MyApp.AppHost/      # Aspire orchestration
├── deploy/
│   ├── aws/               # Terraform for AWS
│   ├── gcp/               # Terraform for GCP
│   └── azure/             # Terraform for Azure
└── ndc.json              # NDC configuration

Aspire AppHost Setup

// Program.cs in AppHost
var builder = DistributedApplication.CreateBuilder(args);

// Add backing services
var postgres = builder.AddPostgres("postgres");
var redis = builder.AddRedis("redis");
var minio = builder.AddMinIO("storage");

// Add your API
builder.AddProject<Projects.MyApp_Api>("api")
    .WithReference(postgres)
    .WithReference(redis)
    .WithReference(minio);

builder.Build().Run();

Cloud Deployment Examples

AWS Deploy to AWS

# Configure AWS credentials
$ aws configure

# Deploy to AWS App Runner + RDS + ElastiCache
$ ndc deploy --target aws --region us-east-1

# Creates:
# - App Runner service for your API
# - RDS PostgreSQL instance
# - ElastiCache Redis cluster

GCP Deploy to Google Cloud

# Authenticate with Google Cloud
$ gcloud auth login
$ gcloud config set project YOUR_PROJECT_ID

# Deploy to Cloud Run + Cloud SQL + Memorystore
$ ndc deploy --target gcp --region us-central1

# Creates:
# - Cloud Run service for your API
# - Cloud SQL PostgreSQL instance
# - Memorystore Redis instance

Azure Deploy to Microsoft Azure

# Login to Azure
$ az login

# Deploy to Container Apps + SQL Database + Redis
$ ndc deploy --target azure --region eastus

# Creates:
# - Container Apps for your API
# - Azure SQL Database
# - Azure Cache for Redis

Configuration

// ndc.json configuration file
{
  "name": "myapp",
  "version": "1.0.0",
  "targets": {
    "aws": {
      "region": "us-east-1",
      "services": {
        "database": "rds-postgres",
        "cache": "elasticache-redis",
        "compute": "app-runner"
      }
    },
    "gcp": {
      "project": "your-project-id",
      "region": "us-central1",
      "services": {
        "database": "cloud-sql-postgres",
        "cache": "memorystore-redis",
        "compute": "cloud-run"
      }
    },
    "azure": {
      "subscription": "your-subscription-id",
      "region": "eastus",
      "services": {
        "database": "azure-sql",
        "cache": "azure-redis",
        "compute": "container-apps"
      }
    }
  }
}

Noundry.Blazor

60+ production-ready Blazor components built on Tailwind CSS for Blazor WebAssembly and Blazor Server applications.

Installation

$ dotnet add package Noundry.Blazor

Component Categories

📐 Layout & Navigation

AppShell, Sidebar, Menu, Breadcrumb, Tabs

📝 Content Display

Cards, Accordion, Table, Modal, Slideover

🔘 Form Elements

Button, ProgressButton, Input, Dropdown

💬 Feedback & Indicators

Alert, Toast, Progress, Spinner, Badge

Quick Example

@page "/demo"
@using Noundry.Blazor.Button
@using Noundry.Blazor.Card

<Card Title="Welcome" ButtonText="Learn More">
    Beautiful Blazor components built on Tailwind CSS.
</Card>

<Button BackgroundColor="bg-purple-600"
        OnButtonClick="HandleClick">
    Click Me
</Button>

📚 Full Documentation

For complete component reference, installation guide, and examples, visit the Blazor UI documentation page.

View Complete Blazor UI Docs →

Noundry.Jobs

Job scheduling library with CLI tool for creating and managing automated tasks across Windows, Linux, and macOS.

Installation

Library Package

$ dotnet add package Noundry.Jobs

CLI Tool

$ dotnet tool install --global Noundry.Jobs.Tool

Features

🗄️ Database Operations

Execute queries on SQL Server, PostgreSQL, MySQL, SQLite

📁 File Operations

Copy, move, rename, delete files with async support

🌐 HTTP/API Client

Make API calls with Bearer/Basic/API Key authentication

📧 Email (SMTP)

Send emails via SMTP with attachments and HTML support

Quick Example

#!/usr/bin/env dotnet-script
#r "nuget: Noundry.Jobs, 1.0.0"

using Noundry.Jobs.Database;

var db = new JobsDb(connectionString, DatabaseType.SqlServer);

// Clean up old records
await db.ExecuteAsync("DELETE FROM Logs WHERE CreatedAt < @Date",
    new { Date = DateTime.UtcNow.AddDays(-30) });

// Schedule with CLI: njobs create DailyCleanup cleanup.csx "0 2 * * *"

📚 Full Documentation

For complete API reference, CLI commands, and examples, visit the Jobs documentation page.

View Complete Jobs Docs →

Noundry Web App Template

Full-stack web application template with authentication, database integration, Entity Framework migrations, and responsive UI built with Noundry libraries.

Quick Start

# Create a new web application
$ dotnet new noundry-app -n MyWebApp
# Navigate to project
$ cd MyWebApp
# Run the application
$ dotnet run

What's Included

Authentication & Authorization

  • • ASP.NET Core Identity
  • • Role-based access control
  • • OAuth providers (Google, GitHub)

Database

  • • Entity Framework Core
  • • Code-first migrations
  • • SQL Server / PostgreSQL support

UI Components

  • • Noundry.UI component library
  • • Tailwind CSS integration
  • • Responsive design

Developer Experience

  • • Hot reload support
  • • Logging configured
  • • Docker support

Perfect For

  • • SaaS applications
  • • Internal tools and dashboards
  • • Customer-facing web portals
  • • E-commerce platforms

Noundry API Template

Production-ready REST API template with JWT authentication, Swagger documentation, health checks, structured logging, and comprehensive error handling.

Quick Start

# Create a new API
$ dotnet new noundry-api -n MyApi
# Navigate to project
$ cd MyApi
# Run the API
$ dotnet run
# API available at https://localhost:5001/swagger

What's Included

Authentication

  • • JWT bearer authentication
  • • API key support
  • • Role-based authorization

Documentation

  • • Swagger / OpenAPI 3.0
  • • XML documentation comments
  • • Request/response examples

Observability

  • • Structured logging (Serilog)
  • • Health check endpoints
  • • Request correlation IDs

Production Ready

  • • CORS configuration
  • • Rate limiting
  • • Response caching

Perfect For

  • • Microservices architecture
  • • Mobile app backends
  • • Third-party integrations
  • • Public or partner APIs

NoundryMCP

Model Context Protocol (MCP) server providing AI assistants with access to Noundry documentation, code examples, and project scaffolding capabilities.

What is NoundryMCP?

NoundryMCP is a hosted MCP server that integrates with Claude Desktop and other MCP-compatible AI clients. It gives AI assistants the ability to search Noundry documentation, retrieve code examples, and help you scaffold new Noundry projects directly from your conversations.

Quick Setup with Claude Desktop

1. Locate Your Claude Desktop Config File

The configuration file location depends on your operating system:

Windows
%APPDATA%\Claude\claude_desktop_config.json
macOS
~/Library/Application Support/Claude/claude_desktop_config.json
Linux
~/.config/Claude/claude_desktop_config.json

2. Add NoundryMCP Configuration

Open the config file and add the NoundryMCP server:

{
  "mcpServers": {
    "noundry": {
      "url": "https://mcp.noundry.com"
    }
  }
}

Note: If you already have other MCP servers configured, just add the "noundry" entry to your existing "mcpServers" object.

3. Restart Claude Desktop

Close and reopen Claude Desktop for the changes to take effect. You should now see NoundryMCP listed in your available tools.

Available Tools

search_documentation

Search across all Noundry documentation including libraries, components, CLI tools, and guides.

Example: "How do I use Noundry.Guardian?"

get_code_examples

Retrieve code examples and snippets for specific Noundry libraries and components.

Example: "Show me Sod validation examples"

scaffold_project

Generate project templates and scaffolding commands for new Noundry applications.

Example: "Create a new Blazor app with Noundry"

list_libraries

Get a comprehensive list of all available Noundry libraries with descriptions.

Example: "What libraries does Noundry offer?"

Usage Examples

You: "How do I add input validation to my ASP.NET Core app?"

Claude: Searches documentation and provides Noundry.Sod validation examples with setup instructions.

You: "Create a new web API project with Noundry"

Claude: Provides the commands to install templates and scaffold a new API project with authentication.

You: "Show me how to use Noundry.DotEnvX for environment variables"

Claude: Retrieves code examples showing configuration, encryption, and validation setup.

Benefits

  • • Get instant answers about Noundry features and usage
  • • Access code examples without leaving your conversation
  • • Generate project scaffolding commands on the fly
  • • Learn best practices from AI-powered recommendations
  • • No API keys or authentication required

Noundry Engine CLI

Fast .NET deployment automation for Linux servers. Deploy .NET 9.0+ applications with a single command - no Docker, no complex configuration, just pure speed.

What is Noundry Engine?

Noundry Engine CLI (ndng) automates the complete lifecycle of deploying .NET applications to Linux servers. It eliminates manual server configuration, SSL certificate management, and deployment complexity by providing a single unified interface for all deployment operations.

Installation

$ dotnet tool install --global Noundry.Engine.Cli
# Verify installation
$ ndng --version

Quick Start

Deploy your first .NET application in 3 simple steps:

1. Initialize Your Server

$ ndng init

Installs .NET 9.0, Nginx, SSL certificates, and configures firewall on your Ubuntu server

2. Add Your Application

$ ndng add myapp --test --port 5000

Creates myapp.noundry.app with automatic DNS and SSL

3. Deploy

# Build your application
$ dotnet publish -c Release -o ./publish
# Deploy to server
$ ndng deploy myapp.noundry.app --source ./publish

Key Features

Server Initialization

One-command setup of .NET 9.0, Nginx, SSL tools, and firewall

Free Test Domains

Get *.noundry.app subdomains with automatic DNS and SSL

Database Support

PostgreSQL, MySQL, SQL Server with auto-configuration

Redis & RabbitMQ

Install cache and message queue with one command

Fast Deployment

SFTP-based uploads with progress tracking and auto-restart

CI/CD Integration

Generate GitHub Actions workflows with one command

Command Reference

ndng init

Initialize server with required software (.NET, Nginx, SSL, firewall)

ndng add <domain>

Deploy a new application with SSL and reverse proxy

ndng deploy <domain>

Upload application files and restart service

ndng database

Install PostgreSQL, MySQL, or SQL Server

ndng cache

Install Redis cache server

ndng queue

Install RabbitMQ message queue

ndng list

List all deployments and installed components

ndng log [service]

Stream logs in real-time from systemd journal

ndng github-action <domain>

Generate CI/CD workflow file

Example: Full Deployment Workflow

# 1. Initialize server (one time setup)
$ ndng init \
    --host 192.168.1.100 \
    --key ~/.ssh/id_rsa \
    --email admin@example.com \
    --engine-api-key YOUR_KEY

# 2. Install PostgreSQL database
$ ndng database --type postgres --name myapp_db --user myapp_user

# 3. Install Redis cache
$ ndng cache

# 4. Add application with test domain
$ ndng add myapp --test --port 5000 --email admin@example.com

# 5. Build and deploy your .NET app
$ cd /path/to/your/project
$ dotnet publish -c Release -o ./publish
$ ndng deploy myapp.noundry.app --source ./publish

# 6. View logs
$ ndng log myapp-noundry-app

# Your app is now live at https://myapp.noundry.app! 🚀

Perfect For

  • • Solo developers deploying personal projects
  • • Startups needing fast iteration cycles
  • • Agencies deploying multiple client applications
  • • Development and staging environments
  • • Teams without dedicated DevOps resources

Technology Stack

.NET 9.0

Runtime & ASP.NET Core

Nginx

Reverse Proxy

Let's Encrypt

Free SSL Certificates

Systemd

Service Management