{"id":212,"date":"2026-05-09T11:04:19","date_gmt":"2026-05-09T11:04:19","guid":{"rendered":"https:\/\/www.fabricioruch.ch\/?p=212"},"modified":"2026-05-09T11:04:19","modified_gmt":"2026-05-09T11:04:19","slug":"sqlite-repository-design-in-desktop-apps","status":"publish","type":"post","link":"https:\/\/www.fabricioruch.ch\/?p=212","title":{"rendered":"SQLite Repository Design in Desktop Apps"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Interfaces, Abstract Base Classes, Safe SQL, and Audit-Aware Persistence in Chrona<\/h2>\n\n\n\n<p>Desktop applications often start with direct database access inside view models, commands, or small helper classes.<\/p>\n\n\n\n<p>At first, this looks efficient. A button saves a record. A view model loads a list. A small method opens a SQLite connection, executes SQL, and returns data.<\/p>\n\n\n\n<p>Then the application grows.<\/p>\n\n\n\n<p>More entities appear. More screens need the same data. Audit logging becomes necessary. Formatting rules for dates and times must stay consistent. SQL commands repeat connection logic. Parameters are added manually everywhere. Dynamic sorting or filtering introduces SQL-injection risks. Testing becomes harder because persistence logic is scattered across the UI.<\/p>\n\n\n\n<p>The problem is not SQLite.<\/p>\n\n\n\n<p>The problem is missing repository architecture.<\/p>\n\n\n\n<p>Chrona uses a more disciplined approach: repository interfaces define contracts, abstract base classes provide shared SQLite infrastructure, specialized base classes enforce safety rules, and concrete repositories implement real persistence use cases.<\/p>\n\n\n\n<p>This article analyzes that design as a practical desktop architecture pattern.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Repository Interface Defines the Application Contract<\/h2>\n\n\n\n<p>A repository interface should describe what the application needs from persistence without exposing how the data is stored.<\/p>\n\n\n\n<p>Chrona\u2019s audit repository is a clear example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public interface IAuditRepository\n{\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Schreibt einen Audit-Eintrag. Fehler werden protokolliert und verschluckt,\n    \/\/\/ damit die Hauptoperation nicht fehlschl\u00e4gt.\n    \/\/\/ &lt;\/summary&gt;\n    Task AppendAsync(\n        AuditEntry entry,\n        CancellationToken cancellationToken = default);\n\n    Task&lt;IReadOnlyList&lt;AuditEntry&gt;&gt; GetRecentAsync(\n        int maxCount,\n        CancellationToken cancellationToken = default);\n\n    \/\/\/ &lt;summary&gt;\n    \/\/\/ Entfernt alle Audit-Eintr\u00e4ge. Gibt die Anzahl gel\u00f6schter Zeilen zur\u00fcck.\n    \/\/\/ &lt;\/summary&gt;\n    Task&lt;int&gt; DeleteAllAsync(\n        CancellationToken cancellationToken = default);\n}\n<\/code><\/pre>\n\n\n\n<p>This interface is not just \u201cclean code decoration\u201d.<\/p>\n\n\n\n<p>It protects the rest of the application from SQLite details.<\/p>\n\n\n\n<p>A service or view model that depends on <code>IAuditRepository<\/code> does not need to know about connection strings, SQL commands, parameter binding, table names, or transaction details. It only knows the persistence capability it requires.<\/p>\n\n\n\n<p>That matters in a desktop app because the UI should not become the database orchestration layer. The UI should ask for data or trigger an operation. The repository should own persistence.<\/p>\n\n\n\n<p>The interface also creates a test seam. In tests, <code>IAuditRepository<\/code> can be replaced with an in-memory fake or mock. That allows application logic to be tested without creating a real SQLite database.<\/p>\n\n\n\n<p>The key design point is this:<\/p>\n\n\n\n<p>The interface expresses the persistence contract. It does not leak the persistence mechanism.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Shared SQLite Infrastructure Belongs in an Abstract Base Class<\/h2>\n\n\n\n<p>Without a base repository class, every concrete repository usually repeats the same technical code:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>read connection string<\/li>\n\n\n\n<li>open SQLite connection<\/li>\n\n\n\n<li>create command<\/li>\n\n\n\n<li>add parameters<\/li>\n\n\n\n<li>handle <code>DBNull.Value<\/code><\/li>\n\n\n\n<li>log failures<\/li>\n\n\n\n<li>format values consistently<\/li>\n<\/ul>\n\n\n\n<p>Chrona avoids this with <code>RepositoryBase<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public abstract class RepositoryBase\n{\n    protected readonly string ConnectionString;\n    protected readonly ILogger Logger;\n\n    protected RepositoryBase(\n        IDatabaseInitializer dbInitializer,\n        ILogger logger)\n    {\n        ConnectionString = dbInitializer.ConnectionString;\n        Logger = logger;\n    }\n\n    protected static void AddParameters(\n        SqliteCommand command,\n        params (string name, object? value)&#91;] parameters)\n    {\n        foreach (var (name, value) in parameters)\n        {\n            command.Parameters.AddWithValue(\n                name,\n                value ?? DBNull.Value);\n        }\n    }\n\n    protected async Task&lt;SqliteConnection&gt; CreateConnectionAsync(\n        CancellationToken cancellationToken = default)\n    {\n        var connection = new SqliteConnection(ConnectionString);\n\n        await connection\n            .OpenAsync(cancellationToken)\n            .ConfigureAwait(false);\n\n        return connection;\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>This base class has a narrow and useful responsibility.<\/p>\n\n\n\n<p>It does not know about booking codes, projects, time entries, or audit entries. It only knows common SQLite infrastructure.<\/p>\n\n\n\n<p>That is the correct use of an abstract base class.<\/p>\n\n\n\n<p>The base class removes duplication without taking over domain behavior. Concrete repositories still own their SQL and mapping logic, but they no longer repeat low-level connection and parameter code.<\/p>\n\n\n\n<p>This gives the repository layer consistency.<\/p>\n\n\n\n<p>Every repository opens connections the same way. Every repository uses the same parameter helper. Every repository gets its connection string through the database initializer. Every repository has logging available.<\/p>\n\n\n\n<p>That consistency matters more as the application grows.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why Parameter Helpers Matter<\/h2>\n\n\n\n<p>This helper looks simple:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>protected static void AddParameters(\n    SqliteCommand command,\n    params (string name, object? value)&#91;] parameters)\n{\n    foreach (var (name, value) in parameters)\n    {\n        command.Parameters.AddWithValue(\n            name,\n            value ?? DBNull.Value);\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>But it solves a real persistence problem.<\/p>\n\n\n\n<p>SQLite commands often require many parameters. Without a helper, each repository writes repetitive code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>command.Parameters.AddWithValue(\"@Code\", bookingCode.Code);\ncommand.Parameters.AddWithValue(\"@Title\", bookingCode.Title);\ncommand.Parameters.AddWithValue(\"@IsActive\", bookingCode.IsActive ? 1 : 0);\n<\/code><\/pre>\n\n\n\n<p>The repetition itself is not the main problem. The problem is inconsistency.<\/p>\n\n\n\n<p>One method handles nulls correctly. Another forgets <code>DBNull.Value<\/code>. Another formats a date differently. Another passes a boolean differently.<\/p>\n\n\n\n<p>The helper centralizes one small but recurring concern: safe parameter insertion with null handling.<\/p>\n\n\n\n<p>It also reinforces an important rule:<\/p>\n\n\n\n<p>Values belong in SQL parameters, not string interpolation.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Specialized Base Class for Safe Dynamic SQL<\/h2>\n\n\n\n<p>Parameters protect values.<\/p>\n\n\n\n<p>They do not protect SQL identifiers.<\/p>\n\n\n\n<p>This distinction is critical.<\/p>\n\n\n\n<p>A parameter can safely represent a value:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>WHERE Code = @Code\n<\/code><\/pre>\n\n\n\n<p>But it cannot safely represent a table name or column name:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ORDER BY @ColumnName\n<\/code><\/pre>\n\n\n\n<p>Database engines treat parameters as values, not identifiers. Therefore, if an application supports dynamic table names, column names, sorting, or filtering, it must validate identifiers explicitly.<\/p>\n\n\n\n<p>Chrona addresses this with a specialized base class.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public abstract class ChronaRepositoryBase : RepositoryBase\n{\n    protected abstract IReadOnlySet&lt;string&gt; AllowedTableNames { get; }\n\n    protected abstract IReadOnlySet&lt;string&gt; AllowedColumnNames { get; }\n\n    protected ChronaRepositoryBase(\n        IDatabaseInitializer dbInitializer,\n        ILogger logger)\n        : base(dbInitializer, logger)\n    {\n    }\n\n    protected sealed override void ValidateTableName(string tableName)\n    {\n        if (!AllowedTableNames.Contains(tableName))\n        {\n            throw new ArgumentException(\n                $\"Ung\u00fcltiger Tabellenname: {tableName}\",\n                nameof(tableName));\n        }\n    }\n\n    protected sealed override void ValidateColumnName(string columnName)\n    {\n        if (!AllowedColumnNames.Contains(columnName))\n        {\n            throw new ArgumentException(\n                $\"Ung\u00fcltiger Spaltenname: {columnName}\",\n                nameof(columnName));\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>This is a strong repository-layer design decision.<\/p>\n\n\n\n<p>The class forces concrete repositories to define allowed table and column names. Dynamic identifiers must pass through a whitelist. Invalid identifiers fail immediately.<\/p>\n\n\n\n<p>That matters because desktop apps often assume local databases are automatically safe. That is a mistake. Even local apps can process imported files, user-entered filters, configurable sort orders, or plugin-like data sources. Dynamic SQL must still be treated carefully.<\/p>\n\n\n\n<p>The design separates two safety mechanisms:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL values      \u2192 parameters\nSQL identifiers \u2192 whitelist validation\n<\/code><\/pre>\n\n\n\n<p>That is the correct model.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Concrete Repository: Combining Contract, Infrastructure, and SQL<\/h2>\n\n\n\n<p>A concrete repository in Chrona combines three things:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>IBookingCodeRepository\n    fachlicher Vertrag\n\nChronaRepositoryBase\n    gemeinsame SQLite-Infrastruktur und Identifier-Schutz\n\nBookingCodeRepository\n    konkrete SQL-Implementierung\n<\/code><\/pre>\n\n\n\n<p>The class shape shows this clearly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class BookingCodeRepository\n    : ChronaRepositoryBase, IBookingCodeRepository\n{\n    private static readonly HashSet&lt;string&gt; AllowedTables =\n        new(StringComparer.Ordinal)\n        {\n            \"BookingCodes\",\n            \"TimeEntries\"\n        };\n\n    public BookingCodeRepository(\n        IDatabaseInitializer dbInitializer,\n        IAuditRepository auditRepository,\n        ILogger&lt;BookingCodeRepository&gt; logger)\n        : base(dbInitializer, logger)\n    {\n        _auditRepository = auditRepository;\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>This is the correct kind of inheritance.<\/p>\n\n\n\n<p>The repository does not inherit business behavior from a base class. It inherits infrastructure behavior: connection handling, logging support, identifier validation.<\/p>\n\n\n\n<p>The interface defines what booking-code persistence must provide. The concrete class defines how that persistence works in SQLite.<\/p>\n\n\n\n<p>The injected <code>IAuditRepository<\/code> also shows that persistence operations are not isolated database writes only. They can participate in application-level concerns such as audit logging.<\/p>\n\n\n\n<p>That is realistic desktop architecture.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Create Operation with SQLite and Audit Logging<\/h2>\n\n\n\n<p>The <code>CreateAsync<\/code> method demonstrates the repository pattern in a real use case.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public async Task&lt;BookingCode&gt; CreateAsync(\n    BookingCode bookingCode,\n    CancellationToken cancellationToken = default)\n{\n    using var connection = await CreateConnectionAsync(\n        cancellationToken).ConfigureAwait(false);\n\n    using var command = new SqliteCommand(\n        \"INSERT INTO BookingCodes \" +\n        \"(Code, Title, FreeTextAllowed, IsActive, CreatedAt, UpdatedAt) \" +\n        \"VALUES (@Code, @Title, @FreeTextAllowed, @IsActive, @CreatedAt, @UpdatedAt); \" +\n        \"SELECT last_insert_rowid();\",\n        connection);\n\n    AddParameters(\n        command,\n        (\"@Code\", bookingCode.Code),\n        (\"@Title\", bookingCode.Title),\n        (\"@FreeTextAllowed\", bookingCode.FreeTextAllowed ? 1 : 0),\n        (\"@IsActive\", bookingCode.IsActive ? 1 : 0),\n        (\"@CreatedAt\", SqliteFormat.SqlRoundTripUtc(bookingCode.CreatedAt)),\n        (\"@UpdatedAt\",\n            SqliteFormat.SqlRoundTripUtcNullable(bookingCode.UpdatedAt)\n            ?? (object)DBNull.Value));\n\n    var id = Convert.ToInt32(\n        await command\n            .ExecuteScalarAsync(cancellationToken)\n            .ConfigureAwait(false));\n\n    bookingCode.Id = id;\n\n    await _auditRepository.AppendAsync(\n        new AuditEntry\n        {\n            Entity = AuditEntityNames.BookingCode,\n            EntityId = bookingCode.Id.ToString(),\n            ChangeType = ChangeType.Created,\n            NewValue = AuditSnapshotSerializer.BookingCodeSnapshot(bookingCode),\n            CreatedAt = DateTime.UtcNow\n        },\n        cancellationToken);\n\n    return bookingCode;\n}\n<\/code><\/pre>\n\n\n\n<p>This method shows several important design qualities.<\/p>\n\n\n\n<p>First, the database connection is created through the shared base class. The repository does not manually duplicate connection-string handling.<\/p>\n\n\n\n<p>Second, the SQL command uses parameters for all values. This protects against SQL injection for values and avoids fragile string concatenation.<\/p>\n\n\n\n<p>Third, boolean values are normalized explicitly as <code>1<\/code> or <code>0<\/code>. That is practical for SQLite because SQLite does not have a separate strict boolean storage type in the same way as some other relational databases.<\/p>\n\n\n\n<p>Fourth, timestamps are formatted through a dedicated helper. The repository does not rely on current UI culture or machine locale.<\/p>\n\n\n\n<p>Fifth, the generated ID is assigned back to the entity after <code>last_insert_rowid()<\/code>.<\/p>\n\n\n\n<p>Sixth, the audit record is appended immediately after persistence. This makes the creation operation explainable later.<\/p>\n\n\n\n<p>This is what a repository should do in a desktop business application: persist state consistently and keep the persistence boundary auditable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why Audit Belongs Near the Mutation Boundary<\/h2>\n\n\n\n<p>Audit logging is strongest when it happens close to the actual mutation.<\/p>\n\n\n\n<p>For a create operation, the repository has the final persisted entity and generated ID. That is the right moment to create an audit snapshot.<\/p>\n\n\n\n<p>If audit logging happens later in the UI, the record may be incomplete, inconsistent, or forgotten entirely.<\/p>\n\n\n\n<p>By injecting <code>IAuditRepository<\/code> into <code>BookingCodeRepository<\/code>, Chrona makes audit logging part of the persistence workflow.<\/p>\n\n\n\n<p>That does create a design decision: the repository is not purely a table gateway anymore. It also participates in audit side effects.<\/p>\n\n\n\n<p>This is acceptable in a desktop business app if the rule is consistent and intentional.<\/p>\n\n\n\n<p>The important part is that audit writing is still abstracted behind <code>IAuditRepository<\/code>. The booking-code repository does not know how audit records are stored internally.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">SQLite Formatting Must Be Explicit<\/h2>\n\n\n\n<p>Local persistence bugs often come from culture and timezone assumptions.<\/p>\n\n\n\n<p>Chrona uses a dedicated formatting helper:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>internal static class SqliteFormat\n{\n    public static string SqlDate(DateTime date)\n    {\n        return date.ToString(\n            \"yyyy-MM-dd\",\n            CultureInfo.InvariantCulture);\n    }\n\n    public static string SqlTime(TimeSpan time)\n    {\n        return time.ToString(\n            @\"hh\\:mm\\:ss\",\n            CultureInfo.InvariantCulture);\n    }\n\n    public static string SqlRoundTripUtc(DateTime utc)\n    {\n        return utc.ToString(\n            \"O\",\n            CultureInfo.InvariantCulture);\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>This is small but important.<\/p>\n\n\n\n<p>A desktop app runs on user machines. Those machines may have different regional settings. If date and numeric values are formatted implicitly, the same application may persist different text representations depending on the user environment.<\/p>\n\n\n\n<p>That creates bugs that are hard to reproduce.<\/p>\n\n\n\n<p>Using <code>CultureInfo.InvariantCulture<\/code> prevents this.<\/p>\n\n\n\n<p>Using <code>\"yyyy-MM-dd\"<\/code> for dates produces stable sortable date strings.<\/p>\n\n\n\n<p>Using <code>\"O\"<\/code> for UTC timestamps preserves round-trip precision and avoids ambiguous local-time formatting.<\/p>\n\n\n\n<p>This helper keeps SQLite values predictable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Repository Design Works<\/h2>\n\n\n\n<p>Chrona\u2019s repository design works because it separates concerns without fragmenting the codebase unnecessarily.<\/p>\n\n\n\n<p>The interface defines what the application expects.<\/p>\n\n\n\n<p>The abstract base class handles repeated SQLite infrastructure.<\/p>\n\n\n\n<p>The specialized base class protects dynamic SQL identifiers.<\/p>\n\n\n\n<p>The concrete repository implements actual persistence behavior.<\/p>\n\n\n\n<p>The formatter centralizes SQLite-compatible value representation.<\/p>\n\n\n\n<p>The audit repository records explainable changes.<\/p>\n\n\n\n<p>These pieces work together as one persistence architecture.<\/p>\n\n\n\n<p>A simplified dependency view looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ViewModel \/ Service\n        \u2193\nIBookingCodeRepository\n        \u2193\nBookingCodeRepository\n        \u2193\nChronaRepositoryBase\n        \u2193\nRepositoryBase\n        \u2193\nSQLite\n<\/code><\/pre>\n\n\n\n<p>And for auditing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BookingCodeRepository\n        \u2193\nIAuditRepository\n        \u2193\nAuditRepository\n        \u2193\nSQLite audit table\n<\/code><\/pre>\n\n\n\n<p>This keeps the UI away from SQL while still giving repositories enough control over efficient database operations.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What This Design Avoids<\/h2>\n\n\n\n<p>This architecture avoids several common desktop persistence mistakes.<\/p>\n\n\n\n<p>It avoids SQL inside view models.<\/p>\n\n\n\n<p>It avoids duplicated connection-opening logic.<\/p>\n\n\n\n<p>It avoids inconsistent null handling.<\/p>\n\n\n\n<p>It avoids culture-dependent date persistence.<\/p>\n\n\n\n<p>It avoids raw dynamic SQL identifiers without validation.<\/p>\n\n\n\n<p>It avoids audit logic being scattered across UI workflows.<\/p>\n\n\n\n<p>It avoids tight coupling to concrete persistence implementations in higher layers.<\/p>\n\n\n\n<p>These are practical wins.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Trade-Offs<\/h2>\n\n\n\n<p>This design is not free.<\/p>\n\n\n\n<p>Interfaces add structure, but too many interfaces can become noise if they do not protect real boundaries.<\/p>\n\n\n\n<p>Abstract base classes reduce duplication, but they must stay focused. If a base repository starts containing entity-specific logic, inheritance becomes a liability.<\/p>\n\n\n\n<p>Audit logging inside repositories creates a side effect. That side effect should be documented clearly, especially whether audit failure blocks the main operation or is treated as best effort.<\/p>\n\n\n\n<p>SQLite-specific formatting helpers are useful, but they also make the repository layer explicitly tied to SQLite. That is acceptable if SQLite is the chosen persistence engine. Do not pretend the implementation is database-agnostic if it is not.<\/p>\n\n\n\n<p>The strength of this design is not theoretical purity. The strength is disciplined pragmatism.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Strategy<\/h2>\n\n\n\n<p>This repository architecture supports several useful test levels.<\/p>\n\n\n\n<p>Interface-based tests can mock repositories when testing services or view models.<\/p>\n\n\n\n<p>Formatter tests can verify stable date, time, and UTC formatting.<\/p>\n\n\n\n<p>Repository integration tests can run against a temporary SQLite database and verify actual SQL behavior.<\/p>\n\n\n\n<p>Audit tests can verify that create, update, and delete operations append the expected audit entries.<\/p>\n\n\n\n<p>Identifier-validation tests can verify that only whitelisted tables and columns are accepted.<\/p>\n\n\n\n<p>The most valuable tests are usually integration tests around concrete repositories because SQL errors often appear only when commands hit a real database.<\/p>\n\n\n\n<p>Good test cases include:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Create booking code persists all fields correctly.\nCreate booking code returns generated ID.\nNull UpdatedAt is stored as DBNull.\nCreatedAt uses round-trip UTC format.\nCreate operation appends audit entry.\nInvalid dynamic column name throws exception.\n<\/code><\/pre>\n\n\n\n<p>These tests protect the persistence boundary.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Final Thoughts<\/h2>\n\n\n\n<p>SQLite is a strong choice for desktop applications, but it still needs architecture.<\/p>\n\n\n\n<p>Chrona\u2019s repository design shows a practical model:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>interfaces for persistence contracts<\/li>\n\n\n\n<li>abstract base classes for shared SQLite infrastructure<\/li>\n\n\n\n<li>whitelist validation for dynamic SQL identifiers<\/li>\n\n\n\n<li>concrete repositories for entity-specific SQL<\/li>\n\n\n\n<li>explicit formatting for dates, times, and UTC values<\/li>\n\n\n\n<li>audit integration at the mutation boundary<\/li>\n<\/ul>\n\n\n\n<p>This is the right level of structure for a serious desktop application.<\/p>\n\n\n\n<p>Not over-engineered.<\/p>\n\n\n\n<p>Not ad hoc.<\/p>\n\n\n\n<p>A repository layer should make persistence boring, predictable, testable, and safe. Chrona\u2019s design moves in exactly that direction.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Interfaces, Abstract Base Classes, Safe SQL, and Audit-Aware Persistence in Chrona Desktop applications often start with direct database access inside view models, commands, or small helper classes. At first, this looks efficient. A button saves a record. A view model loads a list. A small method opens a SQLite connection, executes SQL, and returns data. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,33,27,8,25,32,34],"tags":[],"class_list":["post-212","post","type-post","status-publish","format-standard","hentry","category-csharp","category-methods","category-programming-principles","category-software-architecture","category-software-engineering","category-winforms","category-wpf"],"_links":{"self":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/212","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=212"}],"version-history":[{"count":1,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/212\/revisions"}],"predecessor-version":[{"id":213,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=\/wp\/v2\/posts\/212\/revisions\/213"}],"wp:attachment":[{"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=212"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=212"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabricioruch.ch\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=212"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}