A comprehensive .NET 10 source generator and CRUD framework for SQLite databases that automates the creation of CRUD operations, migrations, and gRPC services from C# models. This project provides a production-ready architecture with repository pattern, unit of work, dependency injection, audit logging, and comprehensive code generation capabilities.
- Features
- Project Overview
- Architecture
- Prerequisites
- Installation
- Quick Start
- Usage Examples
- API Reference
- Configuration
- CLI Reference
- Advanced Features
- Testing
- Performance
- Troubleshooting
- Related Projects
- Contributing
- License
- Automatic Code Generation: Generate repository interfaces, implementations, and migrations from entity models
- Complete CRUD Operations: Full Create, Read, Update, Delete operations with async/await support
- SQLite Integration: Native Microsoft.Data.Sqlite support with connection pooling and thread-safe operations
- Repository Pattern: Generic and strongly-typed repositories with LINQ query support
- Unit of Work Pattern: Transaction support with coordinated multi-repository access
- Service Layer Architecture: Well-structured service classes with business logic separation
- Dependency Injection: Complete Microsoft.Extensions.DependencyInjection setup with fluent configuration
- Async Bulk Import/Export: High-performance bulk operations with streaming and progress reporting
- Streaming Support: Process large datasets efficiently without memory overload
- Progress Reporting: Real-time progress tracking for long-running bulk operations
- Enhanced CLI: New commands and improved user experience
- Source Generation: Generate boilerplate code at compile-time
- Audit Logging: Track all entity changes with timestamps, operation types, and user context
- Validation Framework: Entity validation with comprehensive error handling
- Entity Status Tracking: Lifecycle management (Active, Inactive, Deleted)
- Background Tasks: Async background worker service for long-running operations
- Event Bus: Entity change event system for loosely-coupled communication
- Data Export: Export entities to JSON, CSV, and XML formats
- Caching: In-memory caching with configurable expiration policies
- Error Handling: Structured exception hierarchy and middleware integration
- Rate Limiting: Request rate limiting middleware for API protection
- Logging Middleware: Comprehensive request/response logging
- gRPC Services: Generate Protocol Buffer definitions from models
The framework includes five core domain models:
- User: User accounts with authentication, profile, and timestamp tracking
- Product: Product inventory with pricing, stock management, and category relations
- Order: Customer orders with status lifecycle and total calculations
- Category: Product categories with hierarchical support and display ordering
- AuditLog: Complete audit trail for compliance and debugging
SQLite CRUD Generator is designed to accelerate .NET development by eliminating boilerplate code through intelligent code generation. The project demonstrates best practices in enterprise application architecture while remaining lightweight and suitable for everything from microservices to standalone applications.
The project emphasizes:
- Separation of Concerns: Clear boundaries between data, business, and presentation layers
- SOLID Principles: Single responsibility, dependency injection, interface-based design
- Testability: All components designed for unit testing with mockable dependencies
- Extensibility: Pluggable services, middleware, and event handlers
- Developer Experience: Fluent APIs, comprehensive documentation, and realistic examples
┌─────────────────────────────────────────────────────────────────────┐
│ Application Entry Point │
│ (Program.cs / CLI) │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌──────────▼──────────┐ ┌────────▼─────────┐
│ Middleware Stack │ │ CLI Commands │
├─────────────────────┤ ├──────────────────┤
│ • Error Handling │ │ • Generate │
│ • Logging │ │ • Migrate │
│ • Validation │ │ • Validate │
│ • Rate Limiting │ │ • Stats │
└──────────┬──────────┘ └────────┬─────────┘
│ │
└──────────────┬───────────────┘
│
┌──────────────▼──────────────┐
│ Service Layer (Business │
│ Logic) │
├─────────────────────────────┤
│ • UserService │
│ • ProductService │
│ • OrderService │
│ • GenerationService │
│ • DataExportService │
│ • EventBus │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Unit of Work Pattern │
│ (DbContextProvider) │
├─────────────────────────────┤
│ • Transaction Management │
│ • Repository Coordination │
│ • Scope Management │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Data Access Layer │
│ (Repository Pattern) │
├─────────────────────────────┤
│ • UserRepository │
│ • ProductRepository │
│ • OrderRepository │
│ • CategoryRepository │
│ • AuditLogRepository │
│ • Generic IRepository<T> │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Database Connection │
│ (DatabaseConnection) │
├─────────────────────────────┤
│ • SQLite Connection Pool │
│ • Schema Initialization │
│ • Index Management │
│ • Thread-Safe Operations │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ SQLite Database │
│ (crudgenerator.db) │
├─────────────────────────────┤
│ • Users Table │
│ • Products Table │
│ • Orders Table │
│ • Categories Table │
│ • AuditLogs Table │
└─────────────────────────────┘
Presentation / CLI Layer: Command-line interface and entry points
Program.cs: Application bootstrapCLI/: Command parsing and execution
Service Layer: Business logic and orchestration
Services/: User, Product, Order business operationsEvents/: Event publishing for entity changesAttributes/: Code generation markers
Data Access Layer: Repository pattern implementation
Data/Repository.cs: Generic base repositoryInterfaces/IRepository.cs: Contract for data accessData/DbContextProvider.cs: Unit of work implementation
Infrastructure: Cross-cutting concerns
Configuration/: Dependency injection setupMiddleware/: Request processing pipelineCaching/: Data caching layerIntegration/: External API communicationUtilities/: Helper functions and extensions
Domain: Core business entities
Models/: Domain entities (User, Product, Order, Category, AuditLog)Enums/: Entity status and operation typesEvents/: Domain events
- OS: Windows, macOS, or Linux
- .NET SDK: .NET 10.0 or later (Download)
- SQLite: Included with .NET
- IDE: Visual Studio 2022, VS Code, or JetBrains Rider
- Git: For version control
- Docker (Optional): For containerized deployments
- Docker Compose (Optional): For multi-service orchestration
dotnet --version # Should be 10.0.0 or later
dotnet --list-sdks # Verify .NET 10 is installed# Clone the repository
git clone https://github.com/sarmkadan/dotnet-sqlite-crud-generator.git
cd dotnet-sqlite-crud-generator
# Build the project
dotnet build
# Run the application
dotnet run --project src/DotNet.SQLite.CrudGenerator/DotNet.SQLite.CrudGenerator.csproj# Build Docker image
docker build -t dotnet-crud-generator .
# Run in container
docker run -it dotnet-crud-generator# Start all services
docker-compose up
# View logs
docker-compose logs -f app
# Stop services
docker-compose down# Build project
make build
# Run application
make run
# Run tests
make test
# Clean build artifacts
make cleanGet up and running quickly with our Docker images:
# Pull the latest image
docker pull sarmkadan/dotnet-sqlite-crud-generator:latest
# Run the container
docker run -it sarmkadan/dotnet-sqlite-crud-generator
# Run with volume mount for persistence
docker run -v /local/data:/app/data sarmkadan/dotnet-sqlite-crud-generatorversion: '3.8'
services:
crud-generator:
image: sarmkadan/dotnet-sqlite-crud-generator:latest
volumes:
- ./data:/app/data
ports:
- "8080:8080"DATABASE_PATH: Path to SQLite database fileCONNECTION_TIMEOUT: Database connection timeout in secondsMAX_POOL_SIZE: Maximum connection pool size
// Initialize services
var services = new ServiceCollection();
var settings = new DatabaseSettings { FilePath = "app.db" };
services.AddApplicationServices(settings.ConnectionString);
var serviceProvider = services.BuildServiceProvider();
// Initialize database
await serviceProvider.InitializeDatabaseAsync();
// Create a user
var user = new User
{
Username = "john.doe",
Email = "john@example.com",
PasswordHash = "hashed_password",
FirstName = "John",
LastName = "Doe"
};
using var scope = serviceProvider.CreateScope();
var userService = scope.ServiceProvider.GetRequiredService<UserService>();
var createdUser = await userService.CreateAsync(user);
Console.WriteLine($"User created: {createdUser.Username} (ID: {createdUser.Id})");// Get user by ID
var user = await userService.GetByIdAsync(1);
// Get all users
var allUsers = await userService.GetAllAsync();
// Find users with LINQ
var activeUsers = await userService.FindAsync(u => u.IsActive);
// Get with pagination
var (users, total) = await userService.GetPagedAsync(pageNumber: 1, pageSize: 10);var user = await userService.GetByIdAsync(1);
user.Email = "newemail@example.com";
user.UpdatedAt = DateTime.UtcNow;
var updated = await userService.UpdateAsync(user);
Console.WriteLine($"User updated: {updated.Email}");// Soft delete
var success = await userService.DeleteAsync(userId);
// Hard delete (permanent)
await userService.DeletePermanentlyAsync(userId);public async Task ManageUsers(UserService userService)
{
// Create a new user
var user = new User
{
Username = "alice.smith",
Email = "alice@example.com",
PasswordHash = "secure_hash",
FirstName = "Alice",
LastName = "Smith"
};
user = await userService.CreateAsync(user);
Console.WriteLine($"Created user: {user.Username}");
// Search for users
var users = await userService.FindAsync(u => u.Email.Contains("example"));
Console.WriteLine($"Found {users.Count} users");
// Update user
user.LastName = "Johnson";
user = await userService.UpdateAsync(user);
// Delete user
await userService.DeleteAsync(user.Id);
}public async Task ManageInventory(ProductService productService)
{
// Create a product
var product = new Product
{
Name = "Laptop",
Description = "High-performance laptop",
Price = 999.99m,
StockQuantity = 50,
CategoryId = 1
};
product = await productService.CreateAsync(product);
// Calculate total inventory value
var allProducts = await productService.GetAllAsync();
decimal totalValue = allProducts.Sum(p => p.Price * p.StockQuantity);
Console.WriteLine($"Inventory value: ${totalValue:F2}");
// Find low-stock items
var lowStock = await productService.FindAsync(p => p.StockQuantity < 10);
Console.WriteLine($"Items low on stock: {lowStock.Count}");
}public async Task ProcessOrder(OrderService orderService, UserService userService)
{
var user = await userService.GetByIdAsync(1);
var order = new Order
{
UserId = user.Id,
OrderDate = DateTime.UtcNow,
Status = "Pending",
TotalAmount = 299.99m
};
order = await orderService.CreateAsync(order);
Console.WriteLine($"Order created: {order.Id}");
// Update status
order.Status = "Shipped";
order = await orderService.UpdateAsync(order);
// Get user's orders
var userOrders = await orderService.FindAsync(o => o.UserId == user.Id);
Console.WriteLine($"User has {userOrders.Count} orders");
}public async Task ViewAuditLog(AuditLogRepository auditRepo)
{
// Get all changes
var logs = await auditRepo.GetAllAsync();
// Find changes to a specific entity
var userChanges = logs
.Where(l => l.EntityName == "User" && l.EntityId == 1)
.OrderByDescending(l => l.CreatedAt)
.ToList();
foreach (var log in userChanges)
{
Console.WriteLine($"{log.CreatedAt:yyyy-MM-dd HH:mm:ss} - {log.OperationType}: {log.ChangedProperties}");
}
}public async Task BulkImport(ProductService productService, IUnitOfWork unitOfWork)
{
var products = new List<Product>
{
new() { Name = "Product 1", Price = 10m, StockQuantity = 100 },
new() { Name = "Product 2", Price = 20m, StockQuantity = 200 },
new() { Name = "Product 3", Price = 30m, StockQuantity = 300 },
};
using (var transaction = unitOfWork.BeginTransaction())
{
try
{
foreach (var product in products)
{
await productService.CreateAsync(product);
}
await transaction.CommitAsync();
Console.WriteLine($"Imported {products.Count} products");
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}public async Task ExportData(DataExportService exportService)
{
var products = await exportService.GetAllProductsAsync();
// Export to JSON
var json = exportService.ExportToJson(products);
File.WriteAllText("products.json", json);
// Export to CSV
var csv = exportService.ExportToCsv(products);
File.WriteAllText("products.csv", csv);
// Export to XML
var xml = exportService.ExportToXml(products);
File.WriteAllText("products.xml", xml);
}public async Task PaginatedQuery(UserService userService)
{
int pageSize = 20;
int pageNumber = 1;
while (true)
{
var (users, total) = await userService.GetPagedAsync(pageNumber, pageSize);
foreach (var user in users)
{
Console.WriteLine($"{user.Id}: {user.Username}");
}
Console.WriteLine($"Page {pageNumber} of {Math.Ceiling((double)total / pageSize)}");
if (pageNumber * pageSize >= total) break;
pageNumber++;
}
}public async Task UseCaching(ProductService productService)
{
// First call: hits database
var product1 = await productService.GetByIdAsync(1);
Console.WriteLine("First call - hit database");
// Second call: served from cache
var product2 = await productService.GetByIdAsync(1);
Console.WriteLine("Second call - served from cache");
// Cache is automatically invalidated on update
product1.Price = 99.99m;
await productService.UpdateAsync(product1);
// Next call: cache miss, hits database
var product3 = await productService.GetByIdAsync(1);
Console.WriteLine("After update - cache invalidated");
}public async Task HandleErrors(UserService userService)
{
try
{
var user = await userService.GetByIdAsync(99999);
}
catch (RepositoryException ex)
{
Console.WriteLine($"Repository error: {ex.Message}");
}
catch (ValidationException ex)
{
Console.WriteLine($"Validation error: {ex.Message}");
}
catch (GenerationException ex)
{
Console.WriteLine($"Generation error: {ex.Message}");
}
}public void SubscribeToEvents(EventBus eventBus)
{
eventBus.Subscribe<EntityChangedEvent>(async @event =>
{
Console.WriteLine($"Entity changed: {@event.EntityName}");
Console.WriteLine($"Operation: {@event.OperationType}");
Console.WriteLine($"Entity ID: {@event.EntityId}");
await Task.CompletedTask;
});
}public class UserService : IService<User>
{
Task<User> CreateAsync(User entity);
Task<User> GetByIdAsync(int id);
Task<List<User>> GetAllAsync();
Task<User> UpdateAsync(User entity);
Task<bool> DeleteAsync(int id);
Task<List<User>> FindAsync(Expression<Func<User, bool>> predicate);
Task<(List<User>, int)> GetPagedAsync(int pageNumber, int pageSize);
Task<User> AuthenticateAsync(string username, string password);
}public class ProductService : IService<Product>
{
Task<Product> CreateAsync(Product entity);
Task<Product> GetByIdAsync(int id);
Task<List<Product>> GetAllAsync();
Task<Product> UpdateAsync(Product entity);
Task<bool> DeleteAsync(int id);
Task<List<Product>> FindAsync(Expression<Func<Product, bool>> predicate);
Task<(List<Product>, int)> GetPagedAsync(int pageNumber, int pageSize);
Task<decimal> CalculateTotalValueAsync();
Task<List<Product>> GetByPriceRangeAsync(decimal minPrice, decimal maxPrice);
}public class OrderService : IService<Order>
{
Task<Order> CreateAsync(Order entity);
Task<Order> GetByIdAsync(int id);
Task<List<Order>> GetAllAsync();
Task<Order> UpdateAsync(Order entity);
Task<bool> DeleteAsync(int id);
Task<List<Order>> FindAsync(Expression<Func<Order, bool>> predicate);
Task<(List<Order>, int)> GetPagedAsync(int pageNumber, int pageSize);
Task<List<Order>> GetUserOrdersAsync(int userId);
Task<decimal> GetUserTotalSpentAsync(int userId);
}public interface IRepository<T> where T : class
{
Task<T> AddAsync(T entity);
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task<T> UpdateAsync(T entity);
Task<bool> DeleteAsync(int id);
Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate);
}{
"DatabaseSettings": {
"FilePath": "crudgenerator.db",
"ConnectionTimeout": 30,
"MaxPoolSize": 10
},
"CacheSettings": {
"Enabled": true,
"DefaultExpirationMinutes": 60,
"SlidingExpirationMinutes": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}services.AddApplicationServices(connectionString);
// Adds:
// - DbContextProvider (Unit of Work)
// - All repositories (UserRepository, ProductRepository, etc.)
// - All services (UserService, ProductService, etc.)
// - GenerationService
// - DataExportService
// - EventBus
// - Caching layer
// - Middleware componentsGenerate code artifacts from models:
dotnet run -- generate --type migrations
dotnet run -- generate --type grpc
dotnet run -- generate --type repositoriesApply database migrations:
dotnet run -- migrate --target "202401010000_InitialCreate"Validate database schema:
dotnet run -- validateList entities and their properties:
dotnet run -- list --entity UserDisplay database statistics:
dotnet run -- statsvar queue = serviceProvider.GetRequiredService<BackgroundTaskQueue>();
await queue.QueueAsync(async token =>
{
// Long-running operation
await Task.Delay(5000, token);
Console.WriteLine("Background task completed");
});var handler = new WebhookHandler();
await handler.PublishAsync(new WebhookPayload
{
Event = "user.created",
Data = user,
Timestamp = DateTime.UtcNow
});var client = new ExternalApiClient(httpClientFactory);
var response = await client.PostAsync("/api/endpoint", data);
var result = JsonSerializer.Deserialize<ResponseModel>(response);var monitor = new PerformanceMonitor();
using (monitor.Measure("operation_name"))
{
// Code to measure
}
Console.WriteLine($"Execution time: {monitor.GetMetrics()}");Problem: SQLite database is locked.
Solution:
- Ensure only one process accesses the database
- Increase connection timeout:
ConnectionTimeout: 60 - Use
PRAGMA journal_mode=WALfor concurrent access
Problem: Large dataset operations cause memory issues.
Solution:
- Use pagination:
GetPagedAsync(pageNumber, pageSize) - Stream results instead of loading all at once
- Increase heap size:
dotnet run --configuration Release
Problem: Database queries are slow.
Solution:
- Check database indexes
- Use
FindAsyncwith proper predicates to filter early - Enable caching for frequently accessed data
- Analyze query plans with SQLite tools
Problem: Settings validation fails.
Solution:
var settings = new DatabaseSettings { FilePath = "app.db" };
if (!settings.Validate())
{
Console.WriteLine(settings.ValidationErrors);
}Problem: Project fails to build.
Solution:
# Clean NuGet cache
dotnet nuget locals all --clear
# Restore packages
dotnet restore
# Rebuild
dotnet build --no-restoreThe project follows best practices for testability:
// Mock IRepository for unit tests
var mockRepo = new Mock<IRepository<User>>();
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new User { Id = 1, Username = "test" });
// Test service with mocked dependency
var service = new UserService(mockRepo.Object);
var result = await service.GetByIdAsync(1);
Assert.NotNull(result);
Assert.Equal("test", result.Username);Microbenchmarks are located in benchmarks/ and use BenchmarkDotNet.
Run them with:
dotnet run --project benchmarks/dotnet-sqlite-crud-generator.Benchmarks \
--configuration Release -- --filter '*'Results below were measured on an AMD Ryzen 9 5900X, .NET 10, Release build.
| Method | Description | Mean | Allocated |
|---|---|---|---|
ToPascalCase |
snake → PascalCase | 84.2 ns | 144 B |
ToCamelCase |
snake → camelCase | 97.6 ns | 192 B |
ToSnakeCase |
PascalCase → snake_case (span, no regex) | 47.3 ns | 64 B |
ToKebabCase |
PascalCase → kebab-case | 51.8 ns | 64 B |
RemoveWhitespace |
strip all whitespace | 36.9 ns | 56 B |
ToSlug |
URL-safe slug | 308.4 ns | 296 B |
Repeat ×8 |
string.Create copy loop |
28.1 ns | 56 B |
Pluralize |
suffix-based pluralization | 23.7 ns | 40 B |
| Method | Description | Mean | Allocated |
|---|---|---|---|
GetAsync — hit |
ValueTask.FromResult, zero alloc |
51.4 ns | 0 B |
GetAsync — miss |
key not present | 22.8 ns | 0 B |
SetAsync — upsert |
insert or replace entry | 163.2 ns | 120 B |
ExistsAsync — hit |
presence check, zero alloc | 29.6 ns | 0 B |
GetOrSetAsync — hit |
cache hit, factory skipped | 57.1 ns | 0 B |
GetOrSetAsync — miss |
factory invoked, entry stored | 214.5 ns | 168 B |
| Method | Description | Mean | Allocated |
|---|---|---|---|
GetTableName |
pluralise + snake_case (cached) | 34.8 ns | 48 B |
GetColumnName |
attribute check + snake_case (cached) | 37.2 ns | 48 B |
GetConventionInfo |
full model property scan | 1.09 µs | 576 B |
IsValidPropertyName — valid |
span loop, no LINQ | 17.3 ns | 0 B |
IsValidPropertyName — invalid |
early-exit on first bad char | 13.6 ns | 0 B |
GetApiEndpoint |
versioned REST path | 79.4 ns | 112 B |
ToCSharpToSqlConvention |
round-trip name mapping | 49.1 ns | 64 B |
Part of a collection of .NET libraries and tools. See more at github.com/sarmkadan.
The snippets below show two common patterns for using this library alongside other components in a larger application.
Combining generated repositories with a custom domain service:
// Register the generator's services alongside your own
services.AddApplicationServices(connectionString);
services.AddScoped<IOrderFulfillmentService, OrderFulfillmentService>();
// Inject generated repositories directly into a domain service
public class OrderFulfillmentService(IRepository<Order> orders, IRepository<Product> products)
{
public async Task<Order> FulfillAsync(int orderId)
{
var order = await orders.GetByIdAsync(orderId);
var product = await products.GetByIdAsync(order.ProductId);
product.StockQuantity--;
await products.UpdateAsync(product);
order.Status = "Fulfilled";
return await orders.UpdateAsync(order);
}
}Exporting data and forwarding it via webhook:
var exportService = scope.ServiceProvider.GetRequiredService<DataExportService>();
var webhookHandler = scope.ServiceProvider.GetRequiredService<WebhookHandler>();
var products = await exportService.GetAllProductsAsync();
var json = exportService.ExportToJson(products);
await webhookHandler.PublishAsync(new WebhookPayload
{
Event = "products.exported",
Data = json,
Timestamp = DateTime.UtcNow
});Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit changes:
git commit -am 'Add feature' - Push to branch:
git push origin feature/your-feature - Submit a pull request
- Follow C# naming conventions (PascalCase for public members)
- Add XML comments to public methods
- Keep methods focused and under 30 lines
- Use async/await for I/O operations
- Include error handling for edge cases
- Use imperative mood: "Add feature" not "Added feature"
- Start with type:
feat:,fix:,docs:,refactor: - Keep first line under 72 characters
- Reference issues:
Closes #123
MIT License
Copyright (c) 2024-2026 Vladyslav Zaiets
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
- Documentation: See
/docsdirectory - Examples: See
/examplesdirectory - Issues: GitHub Issues
- Discussions: GitHub Discussions
Built by Vladyslav Zaiets - CTO & Software Architect