An alternative database mapper for .NET, instead of Dapper or Entity Framework.
- Thread safe database context (can be used as a singleton)
- Async enumerable interface (combines well with System.Async.Linq)
- Utilizes the DataSource API introduced in .NET 7
- No change tracking
- No navigation properties
- Typed query support avaiable in Typed namespace
- Currently only a Postgres adapter exists
- Not battle tested
dotnet add Cooke.Gnissel
Postgres adapter:
dotnet add Cooke.Gnissel.Npgsql
Setup adapter (provider specific):
var connectionString = "...";
var adapter = new NpgsqlDbAdapter(NpgsqlDataSource.Create(connectionString));
Setup DbContext (general)
var dbContext = new DbContext(new DbOptions(adapter));
public record User(int Id, string Name);
public record Device(int Id, string Name, int UserId);
var allUsers = await dbContext.Query<User>($"SELECT * FROM users").ToArrayAsync();
await dbContext.NonQuery($"INSERT INTO users(name) VALUES('Foo')");
var userId = 1;
var userById = await dbContext.Query<User>($"SELECT * FROM users WHERE id={userId}").SingleAsync();
var devicesWithUserName = await dbContext.Query<(string userName, Device device)>(
$"SELECT u.name, ud.* FROM users AS u JOIN user_devices AS ud ON u.id=ud.user_id")
.ToArrayAsync();
If all columns are null in a complex type, the result will be null for that complex type.
var usersWithDevices = await dbContext.Query<(User user, Device? device)>(
$"SELECT u.*, ud.* FROM users AS u LEFT JOIN user_devices AS ud ON u.id=ud.user_id")
.ToArrayAsync();
var devicesWithUserName = await dbContext.Query<string>(
$"SELECT name FROM {Sql.Inject("users")}")
.ToArrayAsync();
// Stored as JSON in database
public record Document(string title, string body);
var documentTitle = "Development Process";
var documents = await dbContext.Query<Document>(
$"SELECT document FROM documents WHERE document->'title'={documentTitle:jsonb}")
.SingleAsync();
var userIds = await dbContext.Query<int>(
$"SELECT id FROM users", dbReader => dbReader.GetInt32(0))
.SingleAsync();
await dbContext.Transaction(
dbContext.NonQuery($"INSERT INTO users(name) VALUES('foo')"),
dbContext.NonQuery($"INSERT INTO users(name) VALUES('bar')"));
Currently only non queries are supported.
await dbContext.Batch(
dbContext.NonQuery($"INSERT INTO users(name) VALUES('foo')"),
dbContext.NonQuery($"INSERT INTO users(name) VALUES('bar')"));
Some utils are provided for easier consumption of the async enumerable result.
usersWithDevices // Type: IAsyncEnumerable<(User user, Device device)>
.GroupBy(
(u, d) => u, // Group by selector
(u, device) => device, // Element selector
(u, devices) => (u, devices.ToArray()), // Result selector
u => u.id // Group by key selector
);
The Typed namespace includes support for typed quries.
Create a custom DbContext (which may inherit from DbContext but is not required to).
public record User(int Id, string Name);
public record Device(int Id, string Name, int UserId);
public class AppDbContext(DbOptions options) : DbContext
{
public Table<User> Users { get; } = new Table<User>(options);
public Table<Device> Devices { get; } = new Table<Device>(options);
}
var dbContext = new AppDbContext(new DbOptions(adapter));
var allUsers = await dbContext.Users.ToArrayAsync();
var allBobs = await dbContext.Users.Where(x => x.Name == "Bob").ToArrayAsync();
var allNames = await dbContext.Users.Select(x => x.Name).ToArrayAsync();
var partialDevcies = await dbContext.Devices.Select(x => new { x.Id, DeviceName = x.Name }).ToArrayAsync();
var firstUser = await dbContext.Users.First();
var firstOrNoUser = await dbContext.Users.FirstOrDefault();
var firstBob = await dbContext.Users.First(x => x.Name == "Bob");
var firstOrNoBob = await dbContext.Users.FirstOrDefault(x => x.Name == "Bob");
Supported join types are: (inner) join, left join, right join, full (outer) join and cross join.
(User, Device)[] bobWithDevices = await dbContext.Users
.Join(dbContext.Devices, (u, d) => u.Id == d.UserId)
.Where((u, d) => u.Name == "Bob")
.ToArrayAsync();
await dbContext.Users.Insert(new User(0, "Bob"));
await dbContext.Users.Insert(new User(1, "Alice"), new User(2, "Greta"));
await dbContext.Users.Set(x => x.Name, "Robert").Where(x => x.Name == "Bob");
await dbContext.Users.Set(x => x.LastLogin, null).WithoutWhere();
await dbContext.Users.Delete().Where(x => x.Name == "Bob");
await dbContext.Users.Delete().WithoutWhere();
var userSummaryByName = await dbContext.Users
.GroupBy(x => x.Name)
.Select(x => new {
x.Name,
Count = Db.Count(),
MaxAge = Db.Max(x.Age),
MinAge = Db.Min(x.Age),
MaxAge = Db.Sum(x.Age),
AvgAge = Db.Avg(x.Age)
}).ToArrayAsync();
var userSummaryByName = await dbContext.Users
.OrderBy(x => x.Name)
.ThenByDesc(x => x.Age)
.ToArrayAsync();