Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kant2002/sqlmarshal
Generates data access using stored procedures
https://github.com/kant2002/sqlmarshal
aot csharp-sourcegenerator made-by-ukrainians nativeaot orm
Last synced: 2 days ago
JSON representation
Generates data access using stored procedures
- Host: GitHub
- URL: https://github.com/kant2002/sqlmarshal
- Owner: kant2002
- License: other
- Created: 2021-03-05T04:29:53.000Z (almost 4 years ago)
- Default Branch: main
- Last Pushed: 2024-11-30T14:25:39.000Z (27 days ago)
- Last Synced: 2024-12-18T11:06:56.736Z (9 days ago)
- Topics: aot, csharp-sourcegenerator, made-by-ukrainians, nativeaot, orm
- Language: C#
- Homepage:
- Size: 155 KB
- Stars: 59
- Watchers: 4
- Forks: 6
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: License.txt
Awesome Lists containing this project
README
SqlMarshal
===========================[![NuGet](https://img.shields.io/nuget/v/SqlMarshal.svg?style=flat)](https://www.nuget.org/packages/SqlMarshal/)
NativeAOT-friendly mini-ORM which care about nullability checks.
This project generates typed functions for accessing custom SQL and stored procedures. Goal of this project to be AOT friendly.
Database connection can be used from the DbContext of DbConnection objects.# How to use
Add `SqlMarshal` Nuget package using
```
dotnet add package SqlMarshal
```Then create your data context class inside your project.
```csharp
public class PersonInformation
{
public int PersonId { get; set; }public string? PersonName { get; set; }
}public partial class DataContext
{
private DbConnection connection;public DataContext(DbConnection connection) => this.connection = connection;
[SqlMarshal("persons_list")]
public partial IList GetPersons();[SqlMarshal]
public partial IList GetPersonFromSql([RawSql]string sql, int id);
}
```You can have simplified repository generator.
Detection happens by name, and I plan to automatically support more naming conventions.```csharp
public class Product
{
public int Id { get; set; }public string? Code { get; set; }
public string? ProductTitle { get; set; }
public string? Description { get; set; }
}public partial class DataContext
{
private DbConnection connection;public DataContext(DbConnection connection) => this.connection = connection;
public partial IList FetchAll();
public partial Product? FetchById(int id);
public partial int Count();
public partial void DeleteAll();
public partial void DeleteById(int id);
public partial void Insert(int id, string code, string productTitle, string description);
public partial void Update(int id, string code, string productTitle, string description);
}
```You can override column names and table names of the entity using attributes from `System.ComponentModel.DataAnnotations.Schema` namespace.
# Temporary limitations or plans
Current version of library has several limitations which not because it cannot be implemented reasonably,
but because there was lack of time to think through all options. So I list all current limitations, so any user would be aware about them.
I think about these options like about plan to implement them.- No ability to specify length of input/output string parameters, or type `varchar`/`nvarchar`.
- Simplified ORM for just mapping object properties from DbDataReader
- Ability to specify fields in code in the order different then returned from SQL.
- Automatic generation of DbSet inside DbContext, since when working with stored procedures this is most likely burden.
- FormattableString support not implemented.# Examples
- [DbConnection examples](#dbconnection-examples)
- [Stored procedures which returns resultset](#stored-procedures-which-returns-resultset)
- [Adding parameters](#Adding-parameters)
- [Executing SQL](#Executing-SQL)
- [Output parameters](#Output-parameters)
- [Procedure which returns single row](#Procedure-which-returns-single-row)
- [Scalar results](#Scalar-results)
- [Sequences](#Sequence-results)
- [INSERT or UPDATE](#Without-results)
- [Join transactions](#Join-transactions)
- [DbContext examples](#dbcontext-examples)
- [Stored procedures which returns resultset](#stored-procedures-which-returns-resultset-1)
- [Adding parameters](#Adding-parameters-1)
- [Output parameters](#Output-parameters-1)
- [Procedure which returns single row](#Procedure-which-returns-single-row-1)
- [Scalar results](#Scalar-results-1)
- [INSERT or UPDATE](#Without-results-1)
- [Join transactions](#Join-transactions-1)
- [Alternative options](#Alternative-options)
- [Async methods](#Async-methods)
- [Nullable parameters](#Nullable-parameters)
- [Bidirectional parameters](#Bidirectional-parameters)
- [Pass connection as parameter](#pass-connection-as-parameter)
- [Pass transaction as parameter](#pass-transaction-as-parameter)
- [CancellationToken support](#CancellationToken-support)## Managing connections
Generated code does not interfere with the connection opening and closing. It is responsibility of developer to properly wrap code in the transaction and open connections.
```csharp
public partial class DataContext
{
private DbConnection connection;public DataContext(DbConnection connection) => this.connection = connection;
[SqlMarshal("persons_list")]
public partial IList GetResult();
}
...var connection = new SqlConnection("......");
connection.Open();
try
{
var dataContext = new DataContext(connection);
var items = dataContext.GetResult();
// Do work on items here.
}
finally
{
connection.Close();
}
```Same rule applies to code which uses DbContext.
## Additional samples
In the repository located sample application which I use for testing, but they can be helpful as usage examples.
- https://github.com/kant2002/SqlMarshal/tree/main/SqlMarshal.CompilationTests
## Performance
Now I only hope (because no measurements yet) that performance would be on par with [Dapper](https://github.com/StackExchange/Dapper) or better.
At least right now generated code is visible and can be reason about.## DbConnection examples
### Stored procedures which returns resultset
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("persons_list")]
public partial IList GetResult();
}
```This code translated to `EXEC persons_list`.
When generated code retrieve data reader it starts iterating properties in the `Item` class in the
same order as they are declared and read values from the row. Order different then declaration order not supported now.### Adding parameters
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("persons_search")]
public partial IList GetResults(string name, string city);
}
```This code translated to `EXEC persons_search @name, @city`. Generated code do not use named parameters.
### Executing SQL
If stored procedure seems to be overkill, then you can add string parameter with attribute [RawSql]
and SQL passed to the function would be executed.```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal]
public partial IList GetResultFromSql([RawSql]string sql, int maxId);
}
```### Output parameters
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("persons_search_ex")]
public partial IList GetResults2(string name, string city, out int totalCount);
}
```This code translated to `EXEC persons_search @name, @city, @total_count OUTPUT`.
Value returned in the @total_count parameter, saved to the `int totalCount` variable.### Procedure which returns single row
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("persons_by_id")]
public partial Item GetResults(int personId);
}
```This code translated to `EXEC persons_by_id @person_id`. From mapped result set taken just single item, first one.
### Scalar results
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("total_orders")]
public partial int GetTotal(int clientId);
}
```This code translated to `EXEC total_orders @client_id`. Instead of executing over data reader, ExecuteScalar called.
### Sequence results
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("total_orders")]
public partial IList GetStrings(int clientId);
}
```This code translated to `EXEC total_orders @client_id`. First columns of the returning result set mapped to the sequence.
If you want return more then one columns, and do not want create classes, you can use tuples```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("total_orders")]
public partial IList<(string, int)> GetPairs(int clientId);
}
```### Join transactions
Not implemented.
### Without results
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("process_data")]
public partial void ProcessData(int year);
}
```This code translated to `EXEC process_data @year`. No data was returned, ExecuteNonQuery called.
## DbContext examples
### Stored procedures which returns resultset
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("persons_list")]
public partial IList GetResult();
}
```This code translated to `EXEC persons_list`.
Underlying assumption that in the custom context there definition of the `DbSet`.### Adding parameters
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("persons_search")]
public partial IList GetResults(string name, string city);
}
```This code translated to `EXEC persons_search @name, @city`. Generated code do not use named parameters.
### Output parameters
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("persons_search_ex")]
public partial IList GetResults2(string name, string city, out int totalCount);
}
```This code translated to `EXEC persons_search @name, @city, @total_count OUTPUT`.
Value returned in the @total_count parameter, saved to the `int totalCount` variable.### Procedure which returns single row
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("persons_by_id")]
public partial Item GetResults(int personId);
}
```This code translated to `EXEC persons_by_id @person_id`. From mapped result set taken just single item, first one.
### Scalar results
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("total_orders")]
public partial int GetTotal(int clientId);
}
```This code translated to `EXEC total_orders @client_id`. Instead of executing over data reader, ExecuteScalar called.
### Without results
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("process_data")]
public partial void ProcessData(int year);
}
```This code translated to `EXEC process_data @year`. No data was returned, ExecuteNonQuery called.
### Join transactions
Generated code automatically join any transaction opened using `DbContext.Database.BeginTransaction()`.
## Alternative options
### Async methods
```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("total_orders")]
public partial Task GetTotal(int clientId);
}
```or
```csharp
public partial class DataContext
{
private CustomDbContext dbContext;[SqlMarshal("persons_search")]
public partial Task> GetResults(string name, string city);
}
```### Nullable parameters
The codegen honor nullable parameters. If you specify parameter as non-nullable, it will not work with NULL values in the database,
if you specify that null allowed, it properly convert NULL to null values in C#.```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("get_error_message")]
public partial string? GetErrorMessage(int? clientId);
}
```### Bidirectional parameters
If you have parameters which act as input and output parameters, you can specify them as `ref` values.
Codegen read values after SQL was executed.```csharp
public partial class DataContext
{
private DbConnection connection;[SqlMarshal("get_error_message")]
public partial string? GetErrorMessage(ref int? clientId);
}
```### Pass connection as parameter
Instead of having DbConnection as a field of the class, it can be passed as parameter, and even be placed in the extension method.
```csharp
public static partial class DataContext
{
[SqlMarshal("persons_list")]
public static partial IList GetResult(DbConnection connection);[SqlMarshal("persons_by_id")]
public static partial Item GetResults(DbConnection connection, int personId);
}
```### Pass transaction as parameter
If you want finegrained control over transactions, if you pass `DbTransaction` as parameter, generated code will set it to `DbCommand` or EF context will join that transaction using `Database.UseTransaction`.
```csharp
public static partial class DataContext
{
[SqlMarshal("persons_list")]
public static partial IList GetResult(DbTransaction tran);[SqlMarshal("persons_by_id")]
public static partial Item GetResults(DbTransaction tran, int personId);
}
```### CancellationToken support
You can add CancellationToken inside your code and it would be propagated inside ADO.NET calls.
You can use that with DbContext too.```csharp
public static partial class DataContext
{
[SqlMarshal("total_orders")]
public partial Task GetTotal(DbConnection connection, int clientId, CancellationToken cancellationToken);
}
```