Branch Status
main Dotnet 9

SharpPersistence

SharpPersistence is a collection of robust, extensible, and thoroughly tested .NET libraries for working with database systems.

Installation

To install the packages, run the following commands:

dotnet add package SharpPersistence.Abstractions
dotnet add package SharpPersistence
dotnet add package SharpPersistence.EfCore

Or visit:

Features

SqlParser

Basic Usage

1. Create SQL files with tagged blocks:

-- #start# GetAllUsers
SELECT u.Id, u.Name, u.Email, u.CreatedAt
FROM Users u
WHERE u.Id = :userId
ORDER BY u.Name
-- #end# GetAllUsers

-- #start# GetActiveUsers
SELECT u.Id, u.Name, u.Email
FROM Users u
WHERE u.IsActive = 1 AND u.IsDeleted = 0
ORDER BY u.LastLoginDate DESC
-- #end# GetActiveUsers

2. Configure your project to copy SQL files:


<PropertyGroup>
    <DefaultItemExcludes>
        $(DefaultItemExcludes);out/**;publish/**;bin/**;obj/**
    </DefaultItemExcludes>
</PropertyGroup>

<ItemGroup>
<Content Include="**\*.sql"
         Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)"
         CopyToOutputDirectory="PreserveNewest"
         TargetPath="sharp_persistence_sql_files\%(RecursiveDir)\%(Filename)%(Extension)"/>
</ItemGroup>

<Project Sdk="Microsoft.NET.Sdk">
    <!-- paste it here -->
</Project>

3. Parse and use in your application:

// Parse SQL files at startup
IParsedSqlStorage parsedSqlStorage = new SqlParser().ParseFromStorage();

// Access parsed SQL by tag name (case-insensitive) through C# indexer
string getAllUsersQuery = parsedSqlStorage["GetAllUsers"];
// Use the query...

// Safe access with TryGet
if (parsedSqlStorage.TryGetParsedSql("GetAllUsers", out string? query))
{
    // Use the query...
}

4. Dependency Injection (ASP.NET Core):

// In Program.cs
IParsedSqlStorage parsedSqlStatements = new SqlParser().ParseFromStorage();
builder.Services.AddSingleton<IParsedSqlStorage>(parsedSqlStatements);

// In your service classes
public class UserService
{
    private readonly IDbConnectionFactory _dbConnectionFactory;
    private readonly IParsedSqlStorage _parsedSqlStorage;

    public UserService(IDbConnectionFactory dbConnectionFactory, IParsedSqlStorage sqlStorage)
    {
        _dbConnectionFactory = dbConnectionFactory;
        _parsedSqlStorage = parsedSqlStorage;
    }

    public async Task<IEnumerable<User>> GetAllUsersAsync(int userId, CancellationToken ct)
    {
        var sql = _parsedSqlStorage["GetAllUsers"];

        await using var connection = await _dbConnectionFactory.CreateConnectionAsync(ct);

        var parameters = new { userId };

        return await connection.QueryAsync<User>(new CommandDefinition(sql, parameters,
            cancellationToken: ct));
    }
}

Notes: SqlParser validates your SQL files at startup and throws detailed error messages for:

SqlCheckConstrainGenerator

Some examples with IEntityTypeConfiguration of Ef Core are given below:

  var cc = new SqlCheckConstrainGenerator(Rdbms.PostgreSql, SqlNamingConvention.LowerSnakeCase,
      delimitStringGlobalLevel: false);

  builder.ToTable(x => x.HasCheckConstraint(
      "valid_product_sell_price_in_sales_invoice",
      cc.GreaterThanOrEqual(nameof(SalesInvoiceItem.SingleUnitSellPrice),
          0, SqlDataType.Decimal)
  ));

  builder.ToTable(x => x.HasCheckConstraint(
    "valid_employee_release_data",
    cc.Or(
        cc.And(
            cc.EqualTo(nameof(EmploymentRecord.IsReleased), true),
            cc.IsNotNull(nameof(EmploymentRecord.ReleaseDate)),
            cc.IsNotNull(nameof(EmploymentRecord.ReleaseReasonId))
        ),
        cc.And(
            cc.EqualTo(nameof(EmploymentRecord.IsReleased), false),
            cc.IsNull(nameof(EmploymentRecord.ReleaseDate)),
            cc.IsNull(nameof(EmploymentRecord.ReleaseReasonId))
        )
    )
));

  builder.ToTable(x => x.HasCheckConstraint(
    "valid_debit_credit_entry",
    cc.Or(
        cc.And(
            cc.GreaterThanOrEqual(nameof(JournalVoucherItem.CreditAmount),
                0, SqlDataType.Decimal),
            cc.EqualTo(nameof(JournalVoucherItem.DebitAmount),
                0, SqlDataType.Decimal)
        ),
        cc.And(
            cc.GreaterThanOrEqual(nameof(JournalVoucherItem.DebitAmount),
                0, SqlDataType.Decimal),
            cc.EqualTo(nameof(JournalVoucherItem.CreditAmount),
                0, SqlDataType.Decimal)
        )
    )
));

RepositoryBase with IRepositoryBase

UnitOfWork with IUnitOfWork

Testing

Contributing

Contributions are welcome! Please open issues or submit pull requests for bug fixes, improvements, or new features.