Welcome to the comprehensive documentation for the Noundry platform. Explore our libraries, components, and tools to accelerate your .NET development.
Start with our project templates for rapid development.
Common patterns and code snippets for rapid development.
A fluent assertion library that makes your tests more readable and expressive.
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");
// 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));
// 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]+$");
});
}
Lightweight guard clauses for defensive programming and input validation.
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 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");
// 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...
}
Zod-inspired schema validation for .NET with fluent API, type-safe validation, and comprehensive error messages.
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}");
}
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();
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());
// 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();
Complete UI component library with Alpine.js integration and Tailwind CSS styling.
<!-- 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>
<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>
Are you sure you want to delete this item?
<noundry-input
label="Email Address"
type="email"
placeholder="Enter your email"
required="true"
/>
A powerful, lightweight form validation library for modern web applications. Built for vanilla JavaScript and Alpine.js integration.
Custom <noundry-element> tag for seamless integration
Leverages built-in browser validation
Monitor dirty, valid, and submitting states
Seamless reactive integration
<!-- 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>
<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>
Lightweight (~8KB minified) with zero dependencies. Perfect for modern web applications.
View Full Documentation →Modern ORM combining the power of Dapper with additional functionality for rapid data access.
// Program.cs
builder.Services.AddTuxedoSqlServer(connectionString);
// Or for PostgreSQL
builder.Services.AddTuxedoPostgreSQL(connectionString);
// Or for MySQL
builder.Services.AddTuxedoMySQL(connectionString);
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);
}
}
// 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;
}
}
Strongly-typed database exceptions for precise error handling. Transform generic DbException errors into specific, catchable exception types:
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";
}
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");
}
}
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
Production-ready examples for complex data scenarios:
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;
}
}
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>();
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);
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);
}
// 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);
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;
}
}
// 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
};
}
ASP.NET Core TagHelpers optimized for Tailwind CSS with built-in accessibility and responsive design.
<!-- 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>
Sessionless OAuth 2.0 authentication for multiple providers with JWT token management. Secure, scalable, and easy to integrate into any .NET application.
// 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();
<!-- 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>
<!-- 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>
[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));
}
}
Production-ready authentication patterns for secure applications:
// 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");
}
}
}
// 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);
}
}
// 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;
}
}
Production-ready patterns for enterprise applications:
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);
}
}
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);
}
}
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;
}
}
}
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
);
}
}
Comprehensive change tracking for Insert, Update, and Delete operations with automatic audit trail generation:
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
// 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);
// 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");
});
});
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"
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);
// 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)
Automated database schema synchronization library that creates and updates database tables directly from your C# model classes. No manual migrations needed.
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; }
}
// 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
);
[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";
}
# 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
Intelligent fake data generation library and CLI tool for Tuxedo ORM. Generate realistic test data with automatic foreign key and primary key relationship detection.
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);
# 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"
High-performance CSV to database ingestion tool capable of processing millions of rows in seconds with zero-allocation parsing and intelligent schema inference.
# 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
# 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
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}");
Automatically detects column types, sizes, and constraints from your CSV data, creating optimal database schemas without manual configuration.
Automatically creates indexes on commonly queried columns like ID fields and foreign keys for optimal query performance.
Supports various CSV formats with configurable delimiters, quotes, escape characters, and encoding options.
Powerful API connector library for .NET that provides strongly-typed HTTP clients with automatic authentication, LINQ querying, and comprehensive code generation capabilities.
Interactive wizard to generate strongly-typed C# models and Refit interfaces from REST APIs
Built-in authentication with support for Bearer tokens, API keys, and OAuth 2.0
Compile-time safety with strongly-typed models and enhanced IntelliSense
Query API responses like in-memory collections with full LINQ integration
# 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 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);
}
// 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";
});
services.AddTokenAuthentication("your-api-token");
services.AddConnector<IYourApi>(opt => opt.BaseUrl = "https://api.example.com");
services.AddOAuthAuthentication(config =>
{
config.ClientId = configuration["GitHub:ClientId"];
config.ClientSecret = configuration["GitHub:ClientSecret"];
config.TokenEndpoint = "https://github.com/login/oauth/access_token";
});
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);
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();
}
Enhanced .env file support with encryption, validation, and environment-specific configurations. Secure your application secrets while maintaining developer productivity.
// 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;
});
# 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...
# 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
// 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
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();
});
Production-ready patterns for secure 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
// 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();
});
// 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;
}
}
}
# .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
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.
# 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!
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
// 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();
# 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
# 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
# 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
// 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"
}
}
}
}
60+ production-ready Blazor components built on Tailwind CSS for Blazor WebAssembly and Blazor Server applications.
AppShell, Sidebar, Menu, Breadcrumb, Tabs
Cards, Accordion, Table, Modal, Slideover
Button, ProgressButton, Input, Dropdown
Alert, Toast, Progress, Spinner, Badge
@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>
For complete component reference, installation guide, and examples, visit the Blazor UI documentation page.
View Complete Blazor UI Docs →Job scheduling library with CLI tool for creating and managing automated tasks across Windows, Linux, and macOS.
Execute queries on SQL Server, PostgreSQL, MySQL, SQLite
Copy, move, rename, delete files with async support
Make API calls with Bearer/Basic/API Key authentication
Send emails via SMTP with attachments and HTML support
#!/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 * * *"
For complete API reference, CLI commands, and examples, visit the Jobs documentation page.
View Complete Jobs Docs →Full-stack web application template with authentication, database integration, Entity Framework migrations, and responsive UI built with Noundry libraries.
Production-ready REST API template with JWT authentication, Swagger documentation, health checks, structured logging, and comprehensive error handling.
Model Context Protocol (MCP) server providing AI assistants with access to Noundry documentation, code examples, and project scaffolding capabilities.
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.
The configuration file location depends on your operating system:
%APPDATA%\Claude\claude_desktop_config.json
~/Library/Application Support/Claude/claude_desktop_config.json
~/.config/Claude/claude_desktop_config.json
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.
Close and reopen Claude Desktop for the changes to take effect. You should now see NoundryMCP listed in your available tools.
Search across all Noundry documentation including libraries, components, CLI tools, and guides.
Example: "How do I use Noundry.Guardian?"
Retrieve code examples and snippets for specific Noundry libraries and components.
Example: "Show me Sod validation examples"
Generate project templates and scaffolding commands for new Noundry applications.
Example: "Create a new Blazor app with Noundry"
Get a comprehensive list of all available Noundry libraries with descriptions.
Example: "What libraries does Noundry offer?"
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.
Fast .NET deployment automation for Linux servers. Deploy .NET 9.0+ applications with a single command - no Docker, no complex configuration, just pure speed.
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.
Deploy your first .NET application in 3 simple steps:
$ ndng init
Installs .NET 9.0, Nginx, SSL certificates, and configures firewall on your Ubuntu server
$ ndng add myapp --test --port 5000
Creates myapp.noundry.app
with automatic DNS and SSL
# Build your application
$ dotnet publish -c Release -o ./publish
# Deploy to server
$ ndng deploy myapp.noundry.app --source ./publish
One-command setup of .NET 9.0, Nginx, SSL tools, and firewall
Get *.noundry.app
subdomains with automatic DNS and SSL
PostgreSQL, MySQL, SQL Server with auto-configuration
Install cache and message queue with one command
SFTP-based uploads with progress tracking and auto-restart
Generate GitHub Actions workflows with one command
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
# 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! 🚀
Runtime & ASP.NET Core
Reverse Proxy
Free SSL Certificates
Service Management