{"id":31970327,"url":"https://github.com/jchristn/durable","last_synced_at":"2026-01-20T17:29:34.863Z","repository":{"id":318721634,"uuid":"1040894178","full_name":"jchristn/Durable","owner":"jchristn","description":"A lightweight .NET ORM library with LINQ capabilities, designed with a clean, generic architecture that allows developers to build custom repository implementations without being constrained by opinionated base classes","archived":false,"fork":false,"pushed_at":"2025-10-10T18:20:39.000Z","size":2275,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-12T06:39:18.051Z","etag":null,"topics":["database","ef","linq","mysql","orm","postgres","postgresql","sqlite","sqlserver"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jchristn.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-19T17:00:15.000Z","updated_at":"2025-10-10T18:20:42.000Z","dependencies_parsed_at":"2025-10-12T06:39:27.955Z","dependency_job_id":"7623d7b7-f104-4416-9b98-834894115680","html_url":"https://github.com/jchristn/Durable","commit_stats":null,"previous_names":["jchristn/durable"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/jchristn/Durable","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jchristn%2FDurable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jchristn%2FDurable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jchristn%2FDurable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jchristn%2FDurable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jchristn","download_url":"https://codeload.github.com/jchristn/Durable/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jchristn%2FDurable/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279016101,"owners_count":26085802,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","ef","linq","mysql","orm","postgres","postgresql","sqlite","sqlserver"],"created_at":"2025-10-14T19:13:53.661Z","updated_at":"2026-01-20T17:29:34.850Z","avatar_url":"https://github.com/jchristn.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/jchristn/Durable/blob/main/assets/logo.png\" width=\"182\" height=\"182\"\u003e\n\u003c/div\u003e\n\n# Durable ORM\n\n[![NuGet Durable.MySql](https://img.shields.io/nuget/v/Durable.MySql.svg?label=Durable.MySql)](https://www.nuget.org/packages/Durable.MySql/)\n[![NuGet Durable.Postgres](https://img.shields.io/nuget/v/Durable.Postgres.svg?label=Durable.Postgres)](https://www.nuget.org/packages/Durable.Postgres/)\n[![NuGet Durable.Sqlite](https://img.shields.io/nuget/v/Durable.Sqlite.svg?label=Durable.Sqlite)](https://www.nuget.org/packages/Durable.Sqlite/)\n[![NuGet Durable.SqlServer](https://img.shields.io/nuget/v/Durable.SqlServer.svg?label=Durable.SqlServer)](https://www.nuget.org/packages/Durable.SqlServer/)\n\n_**IMPORTANT** Durable is in ALPHA.  We appreciate your patience, feedback, and willingness to test this library in its early stages.  We welcome feedback, issues, and constructive criticism in the [Issues](https://github.com/jchristn/durable/issues) and [Discussions](https://github.com/jchristn/durable/discussions)_\n\nA lightweight .NET ORM library with LINQ capabilities, designed with a clean, generic architecture that allows developers to build custom repository implementations without being constrained by opinionated base classes.\n\n## Quick Start - Hello World\n\nHere's a complete working example using SQLite:\n\n```csharp\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Durable;\nusing Durable.Sqlite;\n\n// 1. Define your entity\n[Entity(\"people\")]\npublic class Person\n{\n    [Property(\"id\", Flags.PrimaryKey | Flags.AutoIncrement)]\n    public int Id { get; set; }\n\n    [Property(\"first_name\", Flags.String, 64)]\n    public string FirstName { get; set; }\n\n    [Property(\"last_name\", Flags.String, 64)]\n    public string LastName { get; set; }\n\n    [Property(\"birthday\")]\n    public DateTime Birthday { get; set; }\n}\n\n// 2. Use the repository\npublic class Program\n{\n    public static async Task Main()\n    {\n        // Create repository (file-based database)\n        SqliteRepository\u003cPerson\u003e repo = new SqliteRepository\u003cPerson\u003e(\"Data Source=myapp.db\");\n\n        // Initialize the table (creates if not exists)\n        repo.InitializeTable(typeof(Person));\n\n        // Create five records\n        List\u003cPerson\u003e people = new List\u003cPerson\u003e\n        {\n            new Person { FirstName = \"Alice\",   LastName = \"Smith\",    Birthday = new DateTime(1990, 3, 15) },\n            new Person { FirstName = \"Bob\",     LastName = \"Johnson\",  Birthday = new DateTime(1985, 7, 22) },\n            new Person { FirstName = \"Carol\",   LastName = \"Williams\", Birthday = new DateTime(1992, 11, 8) },\n            new Person { FirstName = \"David\",   LastName = \"Brown\",    Birthday = new DateTime(1988, 1, 30) },\n            new Person { FirstName = \"Eve\",     LastName = \"Davis\",    Birthday = new DateTime(1995, 5, 12) }\n        };\n\n        IEnumerable\u003cPerson\u003e created = await repo.CreateManyAsync(people);\n        Console.WriteLine(\"Created 5 records:\");\n        foreach (Person p in created)\n        {\n            Console.WriteLine($\"  {p.Id}: {p.FirstName} {p.LastName} - {p.Birthday:yyyy-MM-dd}\");\n        }\n\n        // Retrieve and display all records\n        Console.WriteLine(\"\\nAll records:\");\n        IEnumerable\u003cPerson\u003e all = repo.ReadAll().ToList();\n        foreach (Person p in all)\n        {\n            Console.WriteLine($\"  {p.Id}: {p.FirstName} {p.LastName} - {p.Birthday:yyyy-MM-dd}\");\n        }\n\n        // Modify all records (add 1 year to birthday)\n        Console.WriteLine(\"\\nUpdating birthdays...\");\n        foreach (Person p in all)\n        {\n            p.Birthday = p.Birthday.AddYears(1);\n            await repo.UpdateAsync(p);\n        }\n\n        // Display modified records\n        Console.WriteLine(\"\\nModified records:\");\n        foreach (Person p in repo.ReadAll())\n        {\n            Console.WriteLine($\"  {p.Id}: {p.FirstName} {p.LastName} - {p.Birthday:yyyy-MM-dd}\");\n        }\n\n        // Delete all records\n        int deleted = repo.DeleteAll();\n        Console.WriteLine($\"\\nDeleted {deleted} records.\");\n    }\n}\n```\n\n## Why Durable?\n\n**Durable** was built to address the limitations and overhead that come with heavyweight ORMs. While frameworks like Entity Framework and nHibernate are powerful, they often introduce unnecessary complexity, performance overhead, and lock you into their opinionated ways of doing things.\n\n### Key Benefits\n\n- **No configuration overhead**: No DbContext, no migrations system, no complex model builder configurations\n- **Attributes instead of fluent API**: Simple, declarative entity definitions with `[Entity]` and `[Property]` attributes\n- **No change tracking overhead**: Durable doesn't track every property change on every entity by default\n- **True LINQ support**: Full expression tree parsing for type-safe queries\n- **Multi-database support**: SQLite, MySQL, PostgreSQL, SQL Server - same API\n- **Async from the ground up**: Every operation has async support\n\n## Requirements\n\n- **.NET 8.0** or later\n- **Database versions:**\n  - SQLite 3.8+ (via Microsoft.Data.Sqlite 9.0+)\n  - MySQL 5.7+ / MariaDB 10.2+ (via MySqlConnector 2.3+)\n  - PostgreSQL 12+ (via Npgsql 8.0+)\n  - SQL Server 2016+ (via Microsoft.Data.SqlClient 5.2+)\n\n## Installation\n\n```bash\n# SQLite\ndotnet add package Durable.Sqlite\n\n# MySQL\ndotnet add package Durable.MySql\n\n# PostgreSQL\ndotnet add package Durable.Postgres\n\n# SQL Server\ndotnet add package Durable.SqlServer\n```\n\n## Database Provider Setup\n\n### SQLite\n\n```csharp\nusing Durable.Sqlite;\n\n// Using connection string\nSqliteRepository\u003cPerson\u003e repo = new SqliteRepository\u003cPerson\u003e(\"Data Source=myapp.db\");\n\n// Using settings object\nSqliteRepositorySettings settings = new SqliteRepositorySettings\n{\n    DataSource = \"myapp.db\",\n    Mode = SqliteOpenMode.ReadWriteCreate,\n    CacheMode = SqliteCacheMode.Shared\n};\nSqliteRepository\u003cPerson\u003e repo = new SqliteRepository\u003cPerson\u003e(settings);\n```\n\n### MySQL\n\n```bash\n# Quick start with Docker\ndocker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=mydb mysql:8\n```\n\n```csharp\nusing Durable.MySql;\n\n// Using connection string\nMySqlRepository\u003cPerson\u003e repo = new MySqlRepository\u003cPerson\u003e(\n    \"Server=localhost;Database=mydb;User=root;Password=password;\");\n\n// Using settings object\nMySqlRepositorySettings settings = new MySqlRepositorySettings\n{\n    Hostname = \"localhost\",\n    Database = \"mydb\",\n    Username = \"root\",\n    Password = \"password\",\n    Port = 3306,\n    SslMode = MySqlSslMode.Preferred\n};\nMySqlRepository\u003cPerson\u003e repo = new MySqlRepository\u003cPerson\u003e(settings);\n```\n\n### PostgreSQL\n\n```bash\n# Quick start with Docker\ndocker run -d -p 5432:5432 -e POSTGRES_PASSWORD=password -e POSTGRES_DB=mydb postgres:16\n```\n\n```csharp\nusing Durable.Postgres;\n\n// Using connection string\nPostgresRepository\u003cPerson\u003e repo = new PostgresRepository\u003cPerson\u003e(\n    \"Host=localhost;Database=mydb;Username=postgres;Password=password;\");\n\n// Using settings object\nPostgresRepositorySettings settings = new PostgresRepositorySettings\n{\n    Hostname = \"localhost\",\n    Database = \"mydb\",\n    Username = \"postgres\",\n    Password = \"password\",\n    Port = 5432,\n    SslMode = SslMode.Prefer\n};\nPostgresRepository\u003cPerson\u003e repo = new PostgresRepository\u003cPerson\u003e(settings);\n```\n\n### SQL Server\n\n```bash\n# Quick start with Docker\ndocker run -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=YourStrong@Passw0rd mcr.microsoft.com/mssql/server:2022-latest\n```\n\n```csharp\nusing Durable.SqlServer;\n\n// Using connection string\nSqlServerRepository\u003cPerson\u003e repo = new SqlServerRepository\u003cPerson\u003e(\n    \"Server=localhost;Database=mydb;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=true;\");\n\n// Using settings object\nSqlServerRepositorySettings settings = new SqlServerRepositorySettings\n{\n    Hostname = \"localhost\",\n    Database = \"mydb\",\n    Username = \"sa\",\n    Password = \"YourStrong@Passw0rd\",\n    TrustServerCertificate = true,\n    Encrypt = false\n};\nSqlServerRepository\u003cPerson\u003e repo = new SqlServerRepository\u003cPerson\u003e(settings);\n```\n\n## Defining Entities\n\nEntities require two attributes: `[Entity]` for the table name and `[Property]` for column mappings.\n\n```csharp\nusing Durable;\n\n[Entity(\"people\")]\npublic class Person\n{\n    [Property(\"id\", Flags.PrimaryKey | Flags.AutoIncrement)]\n    public int Id { get; set; }\n\n    [Property(\"first_name\", Flags.String, 64)]\n    public string FirstName { get; set; }\n\n    [Property(\"last_name\", Flags.String, 64)]\n    public string LastName { get; set; }\n\n    [Property(\"email\", Flags.String, 128)]\n    public string Email { get; set; }\n\n    [Property(\"age\")]\n    public int Age { get; set; }\n\n    [Property(\"salary\")]\n    public decimal Salary { get; set; }\n\n    // Nullable value types\n    [Property(\"birth_date\")]\n    public DateTime? BirthDate { get; set; }\n\n    // Enum stored as string by default\n    [Property(\"status\")]\n    public Status Status { get; set; }\n\n    // Enum stored as integer\n    [Property(\"priority\", Flags.Integer)]\n    public Priority Priority { get; set; }\n}\n\npublic enum Status { Active, Inactive, Pending }\npublic enum Priority { Low, Medium, High }\n```\n\n## Basic CRUD Operations\n\n```csharp\n// Create\nPerson person = new Person\n{\n    FirstName = \"John\",\n    LastName = \"Doe\",\n    Email = \"john.doe@example.com\",\n    Age = 30,\n    Salary = 75000m\n};\nPerson created = await repo.CreateAsync(person);\nConsole.WriteLine($\"Created with ID: {created.Id}\");\n\n// Read\nPerson found = await repo.ReadByIdAsync(created.Id);\nIEnumerable\u003cPerson\u003e adults = repo.ReadMany(p =\u003e p.Age \u003e= 18).ToList();\nIEnumerable\u003cPerson\u003e all = repo.ReadAll().ToList();\n\n// Update\nfound.Salary = 80000m;\nawait repo.UpdateAsync(found);\n\n// Delete\nawait repo.DeleteByIdAsync(found.Id);\n// Or delete by predicate\nint deleted = repo.DeleteMany(p =\u003e p.Age \u003c 18);\n```\n\n## Query Builder\n\n```csharp\n// Complex filtering with LINQ\nIEnumerable\u003cPerson\u003e results = repo\n    .Query()\n    .Where(p =\u003e p.Salary \u003e 100000)\n    .Where(p =\u003e p.Age \u003e= 25)\n    .OrderByDescending(p =\u003e p.Salary)\n    .Take(10)\n    .Execute();\n\n// Async execution\nIEnumerable\u003cPerson\u003e results = await repo\n    .Query()\n    .Where(p =\u003e p.Status == Status.Active)\n    .OrderBy(p =\u003e p.LastName)\n    .ExecuteAsync();\n\n// Get executed SQL for debugging\nIDurableResult\u003cPerson\u003e result = await repo\n    .Query()\n    .Where(p =\u003e p.Age \u003e 30)\n    .ExecuteWithQueryAsync();\n\nConsole.WriteLine($\"SQL: {result.Query}\");\nforeach (Person p in result.Result)\n{\n    Console.WriteLine(p.FirstName);\n}\n```\n\n## Relationships\n\n### One-to-Many\n\n```csharp\n[Entity(\"books\")]\npublic class Book\n{\n    [Property(\"id\", Flags.PrimaryKey | Flags.AutoIncrement)]\n    public int Id { get; set; }\n\n    [Property(\"title\", Flags.String, 200)]\n    public string Title { get; set; }\n\n    [Property(\"author_id\")]\n    [ForeignKey(typeof(Author), \"Id\")]\n    public int AuthorId { get; set; }\n\n    [NavigationProperty(\"AuthorId\")]\n    public Author Author { get; set; }\n}\n\n[Entity(\"authors\")]\npublic class Author\n{\n    [Property(\"id\", Flags.PrimaryKey | Flags.AutoIncrement)]\n    public int Id { get; set; }\n\n    [Property(\"name\", Flags.String, 100)]\n    public string Name { get; set; }\n\n    [InverseNavigationProperty(\"AuthorId\")]\n    public List\u003cBook\u003e Books { get; set; } = new List\u003cBook\u003e();\n}\n\n// Loading related data\nIEnumerable\u003cBook\u003e books = repo.Query()\n    .Include(b =\u003e b.Author)\n    .Execute();\n```\n\n## Transactions\n\n```csharp\n// Explicit transactions\nITransaction transaction = await repo.BeginTransactionAsync();\ntry\n{\n    await repo.CreateAsync(person1, transaction);\n    await repo.CreateAsync(person2, transaction);\n    await transaction.CommitAsync();\n}\ncatch\n{\n    await transaction.RollbackAsync();\n    throw;\n}\n```\n\n## Connection Pooling\n\n```csharp\nusing Durable.Sqlite;\n\n// Create factory with custom pool options\nSqliteConnectionFactory factory = \"Data Source=myapp.db\".CreateFactory(options =\u003e\n{\n    options.MinPoolSize = 5;\n    options.MaxPoolSize = 100;\n    options.ConnectionTimeout = TimeSpan.FromSeconds(30);\n    options.IdleTimeout = TimeSpan.FromMinutes(10);\n    options.ValidateConnections = true;\n});\n\nSqliteRepository\u003cPerson\u003e repo = new SqliteRepository\u003cPerson\u003e(factory);\n```\n\n## Optimistic Concurrency\n\n```csharp\n[Entity(\"authors\")]\npublic class Author\n{\n    [Property(\"id\", Flags.PrimaryKey | Flags.AutoIncrement)]\n    public int Id { get; set; }\n\n    [Property(\"name\", Flags.String, 100)]\n    public string Name { get; set; }\n\n    [Property(\"version\")]\n    [VersionColumn(VersionColumnType.Integer)]\n    public int Version { get; set; } = 1;\n}\n\n// Conflict handling\ntry\n{\n    await repo.UpdateAsync(author);\n}\ncatch (OptimisticConcurrencyException ex)\n{\n    Console.WriteLine($\"Expected: {ex.ExpectedVersion}, Actual: {ex.ActualVersion}\");\n}\n```\n\n## SQL Capture for Debugging\n\n```csharp\nSqliteRepository\u003cPerson\u003e repo = new SqliteRepository\u003cPerson\u003e(connectionString);\nrepo.CaptureSql = true;\n\nIEnumerable\u003cPerson\u003e results = repo.ReadMany(p =\u003e p.Age \u003e 25).ToList();\n\nConsole.WriteLine($\"SQL: {repo.LastExecutedSql}\");\nConsole.WriteLine($\"SQL with params: {repo.LastExecutedSqlWithParameters}\");\n```\n\n## Raw SQL\n\n```csharp\n// Execute raw queries\nIEnumerable\u003cPerson\u003e results = repo\n    .FromSql(\"SELECT * FROM people WHERE salary BETWEEN @p0 AND @p1\", null, 50000, 100000)\n    .ToList();\n\n// Execute non-query SQL\nint affected = await repo.ExecuteSqlAsync(\n    \"UPDATE people SET salary = salary * 1.05 WHERE department = @p0\",\n    null, default, \"Engineering\");\n```\n\n## Table Initialization\n\n```csharp\n// Create table if not exists\nrepo.InitializeTable(typeof(Person));\n\n// Initialize multiple tables\nrepo.InitializeTables(new[] { typeof(Person), typeof(Author), typeof(Book) });\n\n// Validate entity definition without creating table\nbool isValid = repo.ValidateTable(typeof(Person), out List\u003cstring\u003e errors, out List\u003cstring\u003e warnings);\n```\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.\n\n## Contributors\n\nSpecial thanks to the following contributors:\n\n- [@joshclopton](https://github.com/JoshClopton) - Josh Clopton\n- [@jchristn](https://github.com/jchristn) - Joel Christner\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.\n\n### Getting Started with Development\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/amazing-feature`)\n3. Make your changes\n4. Run the tests (`dotnet test src/Durable.sln`)\n5. Commit your changes (`git commit -m 'Add some amazing feature'`)\n6. Push to the branch (`git push origin feature/amazing-feature`)\n7. Open a Pull Request\n\n### Code Style\n\nPlease follow the existing code style and conventions outlined in [CLAUDE.md](src/CLAUDE.md).\n\n### Running Tests\n\n```bash\n# Run all tests\ndotnet test src/Durable.sln\n\n# Run tests for a specific database\ndotnet test src/Test.Sqlite/Test.Sqlite.csproj\ndotnet test src/Test.MySql/Test.MySql.csproj\ndotnet test src/Test.Postgres/Test.Postgres.csproj\ndotnet test src/Test.SqlServer/Test.SqlServer.csproj\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjchristn%2Fdurable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjchristn%2Fdurable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjchristn%2Fdurable/lists"}