Copyright Β© 2025 Kvr.Query. All rights reserved.
A lightweight extension for Dapper that provides type-safe querying of related entities with support for one-to-many and one-to-one relationships.
- β¨ Features
- π¦ Installation
- π Quick Start Guide
- π Usage
- π Extension Methods
- π Fluent API
- π Automatic Deduplication
- π Key Detection
- π‘ Best Practices
β οΈ Limitations- π§ Supported Frameworks
- π Version History
- π License
- π€ Contributing
- π¦ Dependencies
- π¬ Support
- π Fluent API for querying related entities
- π Automatic key detection (primary & foreign)
- π Support for one-to-many and one-to-one relationships
- π¦ Nested relationship querying (ThenInclude)
- π‘οΈ Type-safe property selection
- π WHERE and ORDER BY clause support
- π Minimal boilerplate code
- π·οΈ Support for Data Annotations
You can install the package via NuGet Package Manager:
dotnet add package Kvr.Query
Here is a quick start guide to get you up and running with Kvr.Query.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int AddressId { get; set; }
public Address Address { get; set; }
public int UserProfileId { get; set; }
public UserProfile UserProfile { get; set; }
}
public class Order
{
public int Id { get; set; }
public int UserId { get; set; }
public string Description { get; set; }
public User User { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public List<OrderItem> OrderItems { get; set; }
}
public class Address
{
public int Id { get; set; }
public int UserId { get; set; }
public string Street { get; set; }
public User User { get; set; }
}
public class PaymentMethod
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Method { get; set; }
public Order Order { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Description { get; set; }
public Order Order { get; set; }
}
// Query all orders for a user with id 1
// Orders include User, PaymentMethod and OrderItems
// User includes Address
var orders = await connection.Select<Order>()
.IncludeOne(o => o.User)
.ThenIncludeOne(u => u.Address)
.ThenIncludeOne(u => u.UserProfile)
.IncludeOne(o => o.PaymentMethod)
.IncludeMany(o => o.OrderItems)
.Where<User>(u => u.Name, "@userId")
.QueryAsync(new { userId = 1 });
The above query will return all orders for a user with id 1, including the user's address and payment method, and the order items.
It will use primary keys and foreign keys conventions to detect the relationships.
If you want to use attributes to define the relationships, you can use the Key
, ForeignKey
, InverseProperty
attributes same as in Entity Framework.
Or you could use expressions to define the relationships.
var orders = await connection.Select<Order>(o => o.Id)
.Include(o => o.User, o => o.UserId, u => u.Id)
.ThenInclude(u => u.Address, u => u.AddressId, a => a.Id)
.ThenInclude(u => u.UserProfile, u => u.UserProfileId, up => up.Id)
.Include(o => o.PaymentMethod, o => o.PaymentMethodId, p => p.Id)
.Include(o => o.OrderItems, oi => oi.OrderId, i => i.Id)
.Where<User>(u => u.Name, "@userId")
.QueryAsync(new { userId = 1 });
- π
IDbConnection.Select<T>()
- Creates a query for an entity - π
IDbConnection.Select<T>(Expression<Func<T, object>> keySelector)
- Creates a query with a specific key selector- π
keySelector
: The key selector to use for the query.
- π
- π One-to-many relationships
IncludeMany<TChild>(Expression<Func<TParent, ICollection<TChild>>> navigationProperty, Expression<Func<TChild, object>>? foreignKeyProperty = null, Expression<Func<TChild, object>>? childPrimaryKeyProperty = null, Expression<Func<TChild, object>>[]? excludeColumns = null)
- π
navigationProperty
: The navigation property to include. - π
foreignKeyProperty
: Optional. The foreign key property to use for the relationship. If not provided, the foreign key property will be detected by convention and attribute. - π
childPrimaryKeyProperty
: Optional. The primary key property to use for the relationship. If not provided, the primary key property will be detected by convention and attribute. - β
excludeColumns
: Optional. The columns to exclude from the query, normally not needed, but can be used to exclude columns from the query.
- π
- π One-to-one or many-to-one relationships
- π₯
IncludeOne<TChild>(Expression<Func<TParent, TChild>> navigationProperty, Expression<Func<TParent, object>>? navigationForeignKeyProperty = null, Expression<Func<TChild, object>>? childPrimaryKeyProperty = null, bool includeNavigationKeyInSql = false, Expression<Func<TChild, object>>[]? excludeColumns = null)
- π
navigationProperty
: The navigation property to include. - π
navigationForeignKeyProperty
: Optional. The foreign key property to use for the relationship. If not provided, the foreign key property will be detected by convention and attribute. - π
childPrimaryKeyProperty
: Optional. The primary key property to use for the relationship. If not provided, the primary key property will be detected by convention and attribute. - β‘
includeNavigationKeyInSql
: Optional. If true, the navigation key will be included in the SQL query. - β
excludeColumns
: Optional. The columns to exclude from the query, normally not needed, but can be used to exclude columns from the query.
- π
- π₯
- π³ Nested relationships
β οΈ only supports 2 levels of nested relationships.- 1οΈβ£ first level is defined by using
IncludeOne
one-to-one relationship method orIncludeMany
one-to-many relationship method. - 2οΈβ£ second level only supports
ThenIncludeOne
one-to-one relationship method. - π could include multiple
ThenIncludeOne
methods to define same level of nested relationship to one entity (e.g.ThenInclude(u => u.UserProfile).ThenInclude(up => up.User)
) - βΉοΈ It is different from EF, it does not support
ThenIncludeOne
to higher level nested relationships. And not needed to callInclude
method to return to parent entity then callThenInclude
method to include another child entity. ThenIncludeOne<TGrandChild>(Expression<Func<TChild, TGrandChild>> navigationProperty, Expression<Func<TChild, object>>? navigationKeyProperty = null, Expression<Func<TGrandChild, object>>? grandChildPrimaryKeyProperty = null)
- π
navigationProperty
: The navigation property to include. - π
navigationKeyProperty
: Optional. The foreign key property to use for the relationship. If not provided, the foreign key property will be detected by convention and attribute. - π
grandChildPrimaryKeyProperty
: Optional. The primary key property to use for the relationship. If not provided, the primary key property will be detected by convention and attribute.
- π
// Entity Framework Include and ThenInclude
var orders = await context.Orders
.Include(o => o.User)
.ThenInclude(u => u.Address)
.Include(o => o.User)
.ThenInclude(a => a.UserProfile)
....
// Kvr.Query IncludeOne/IncludeMany and ThenIncludeOne
var orders = await connection.Select<Order>()
.IncludeOne(o => o.User)
.ThenIncludeOne(u => u.Address)
.ThenIncludeOne(u => u.UserProfile)
....
- π If an entity includes multiple
IncludeMany
methods, the cartesian cross product of child entities will be returned fromDapper
query (e.g. If order has multipleOrderItems
and multiplePaymentMethods
, the query will return the cartesian cross product ofOrderItems
andPaymentMethods
). - β»οΈ
QueryAsync
method will automatically remove duplication children entities usingDistinct
method by primary key property.
- π» Expressions to define primary key and foreign key properties on
IncludeOne
,IncludeMany
,ThenIncludeOne
methods: - π·οΈ Attribute to define the primary key and foreign key properties entities:
- π
Key
attribute is used to define the primary key. - π
ForeignKey
attribute is used to define the foreign key. - β©οΈ
InverseProperty
attribute is used to define the inverse property.
- π
- π Convention to define the primary key and foreign key properties on entities:
- π Primary key property fields:
Id
.$"{typeof(T).Name}Id"
is used as the primary key.
- π Foreign key property fields:
$"{navigationPropertyInfo.Name}Id"
.$"{navigationPropertyInfo.PropertyType.Name}Id"
is used as the foreign key.
- π Primary key property fields:
- π Please see the test case to detect the primary key and foreign key properties: UtilsTests.cs
- π Use conventions to automatically detect the primary keys and foreign keys in relationship navigation properties.
- π·οΈ Use attributes to define the relationships if you want to use Data Annotations.
- π
Key
attribute is used to define the primary key. - π
ForeignKey
attribute is used to define the foreign key. - β©οΈ
InverseProperty
attribute is used to define the inverse property.
- π
- β‘ Use expression to define keys explicitly.
- Only supports 2 levels of relationships.
- First level relationships are supported by using
IncludeOne
method andIncludeMany
method. - Second level relationships are only supported by using
ThenIncludeOne
method.
- First level relationships are supported by using
- Only includes the same navigation property once, not detection for duplicate navigation properties.
Key
andForeignKey
attributes only support one property, not supporting composite keys.- Lazy loading is not supported, all requests for related entities are eager loading.
- .NET Standard 2.0+
- .NET 5.0+
- .NET 6.0+
- .NET 7.0+
- 1.0.0
- Initial release with support for one-to-many and one-to-one relationships by using conventions, attributes and expressions.
Apache License 2.0 - see LICENSE
Contributions welcome! Please read our Contributing Guide
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature
) - Commit your Changes (
git commit -m 'Add some AmazingFeature'
) - Push to the Branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
- DapperRelMapper - Relationship mapping extension for Dapper
- SqlBuilder
If you encounter any issues or have questions, please file an issue on the GitHub repository.