Mindfire.EFRepository
3.0.0
dotnet add package Mindfire.EFRepository --version 3.0.0
NuGet\Install-Package Mindfire.EFRepository -Version 3.0.0
<PackageReference Include="Mindfire.EFRepository" Version="3.0.0" />
<PackageVersion Include="Mindfire.EFRepository" Version="3.0.0" />
<PackageReference Include="Mindfire.EFRepository" />
paket add Mindfire.EFRepository --version 3.0.0
#r "nuget: Mindfire.EFRepository, 3.0.0"
#:package Mindfire.EFRepository@3.0.0
#addin nuget:?package=Mindfire.EFRepository&version=3.0.0
#tool nuget:?package=Mindfire.EFRepository&version=3.0.0
EFRepository
Use a LINQ-Enabled version of the Repository Pattern for Entity Framework.
Features Include
- Compatible with .NET Framework and .NET Core Frameworks
- Source Generators automatically create extension methods that allow for a very readable fluent syntax. The generated methods start with
ByE.g.
var query = Repo.Query<Users>()
.ByUsernameStartsWith("Bill")
.ByIsDeleted(false)
.ByRegistrationDateIsBefore(startDate)
.AsNoTracking();
- Decoupling Data Context from Code for Better Testability via the
IRepositoryinterface - C# 10
nullablecompatible code is generated - Automatic Update / Insert detection with
.AddOrUpdate - Force Adding of New Object with
.AddNew(object) - FindOne to find objects by key. Will even work with composite keys
- Events for objects being added or modified or deleted
asyncand synchronous methods provided where possible. Chaining to LINQ's async methods is strongly advised (like.CountAsync()).- Store-neutral transactions through
BeginTransactionandInTransactionAsync
Note: If a value is passed into a By but is null, it will be ignored. If you want to get the the value where the field is null, then use the By...IsNull() or By...IsNotNull(). E.g.:
var query = Repo.Query<Users>()
.ByUsernameStartsWith(null); // No users filtered here.
This is helpful if you want to create a "search" where you have some of the values but always have the option to have some of the values.
It is often necessary to separate the server-side LINQ statements from the client side. In those cases use the .AsEnumerable() function. This will convert the IQueryable to in IEnumerable and everything after that statement will happen after the data is retrieved and there will be no attempt to convert those expressions into the query. A good use-case for this is the mapping of an collection of objects returned from the query to a different type.
Transactions
A transaction wraps a set of repository calls. InTransactionAsync commits when the delegate finishes and rolls back when the delegate throws. BeginTransaction returns a handle for the same rules when you want to commit yourself.
await repo.InTransactionAsync(async () =>
{
var order = await repo.Query<Order>()
.ByOrderId(id)
.FirstAsync();
order.Status = Shipped;
repo.AddOrUpdate(order);
repo.AddOrUpdate(audit);
await repo.SaveAsync();
});
using (var transaction = repo.BeginTransaction())
{
repo.AddOrUpdate(order);
repo.Save();
transaction.Commit();
}
Save and SaveAsync still commit on their own when no transaction is open. Inside a transaction they flush changes into that transaction. Commit on the outermost transaction makes the work durable. Dispose a transaction without Commit and the work rolls back.
A nested BeginTransaction joins the transaction already open on that repository session. Every nested transaction has to Commit. The outermost Commit is the one sent to the store. Rollback on any nested transaction rolls the whole transaction back. Disposing the outer transaction while a nested transaction has not committed rolls the whole transaction back.
The transaction belongs to the repository session it was started on. Repositories that share that session share the transaction. RepositoryIsolation.Default leaves isolation to the store. Any other level is passed through, and the store throws when it cannot honor that level.
InTransaction and InTransactionAsync are extension methods on IRepository, so another store implements BeginTransaction and the transaction handle. A store with no multi-document transaction throws from BeginTransaction.
Setting Up
Setting up is easy! Add the Mindfire.EFRepository nuget package to your project, then follow the steps below.
Step 1 - Dependency Registration
In the example below the
// This isn't really necessary, but it's a really good idea. If you have a type that you may not use or if
// you have a type that needs a factory (to use with a using statement, for example), then this pattern
// might be right up your alley
services.AddTransient(typeof(Lazy<>));
services.AddTransient(typeof(Func<>));
// Now register your DataContext
services.AddScoped<DbContext, ExampleDbContext>();
// Now register IRepository
services.AddScoped<IRepository, Repository>();
// Register your other services that depend on IRepository
services.AddScoped<IExampleService, ExampleService>();
Step 2 - Use In Your Project
This example shows a service that depends on an instance of IRepository. This service could then be injected into something like a Controller for API. It is a good idea to map from your EF database objects to domain objects. This controller uses AutoMapper's IMapper interface to map between domain objects and data objects.
public class OrderService : IOrderService
{
protected IMapper Mapper { get; }
protected IRepository Repo { get; }
public OrderService(IMapper mapper, IRepository repository)
{
Mapper = mapper;
Repo = repository;
}
public async Task<Order> GetOrder(int orderId)
{
var order = await Repo.Query<Order>()
.ByOrderId(orderId)
.FirstOrDefaultAsync();
return Mapper.Map<Order>(order);
}
public async Task<Order> AddOrder(Order order)
{
var mapped = Mapper.Map<DB.Order>(order);
mapped.Created = DateTimeOffset.Now;
Repository.AddOrUpdate(mapped);
await Repository.SaveAsync();
return await GetOrder(mapped.OrderId);
}
...
}
Step 3 - Unit Testing
Here is a quick example of how to unit test a service that has a dependency on some data from the IRepository interface. The example below uses XUnit with Shouldly and FakeItEasy.
You'll need to add a reference to the project with your DbContext in it.
...
using EFRepository;
using FakeItEasy;
using Shouldly;
...
public sealed class OrderServiceTests
{
[Theory, InlineData(1)]
public async Task OrderQueryTest(int orderId)
{
// Arrange
var repo = A.Fake<IRepository>();
A.CallTo(() => repo.Query<Data.Order>())
.Returns(GetFakeOrderData().AsQueryable());
var orderService = new OrderService(repo);
// Act
var target = await orderService.GetOrderById(orderId);
// Assert
A.CallTo(() => repo.Query<Data.Order>()).MustHaveHappened();
target
.ShouldNotBeNull()
.OrderId.ShouldBe(orderId);
}
private IEnumerable<Data.Order> GetFakeOrderData() => new[]
{
new Data.Order
{
OrderId = 1,
Created = DateTime.Now,
Email = "homer@compuserv.net"
}
};
}
Interface
Here is the full interface for IRepository. Beyond what is listed in the interface, the source generators create the following prototypes for each of the following types:
Numeric Types (byte, sbyte, short, ushort, int, uint, long, ulong, single, double, decimal, char) and bool
- By{Name}({type}? value)
- By{Name}Not({type}? value) - not generated for bool
- By{Name}GreaterThan({type}? value)
- By{Name}GreaterThanOrEqual({type}? value)
- By{Name}LessThan({type}? value)
- By{Name}LessThanOrEqual({type}? value)
- By{Name}Between({type}? start, {type}? end) - either bound may be null. Not generated for bool. The bounds are exclusive.
DateTime, DateTimeOffset, DateOnly, or TimeOnly
- By{Name}({type}? value)
- By{Name}Not({type}? value)
- By{Name}IsBefore({type}? value)
- By{Name}IsAfter({type}? value)
- By{Name}IsOnOrBefore({type}? value)
- By{Name}IsOnOrAfter({type}? value)
- By{Name}Between({type}? start, {type}? end) - exclusive
- By{Name}OnDate({type}? value) -
DateTimeandDateTimeOffsetonly. Same day, ignore the time - By{Name}Year(int? value), By{Name}Month(int? value), By{Name}Day(int? value) - not generated for
TimeOnly - By{Name}Hour(int? value), By{Name}Minute(int? value) - not generated for
DateOnly
A null date part does not match. An inclusive span is IsOnOrAfter followed by IsOnOrBefore.
repo.Query<User>()
.ByCreatedIsOnOrAfter(start)
.ByCreatedYear(year);
String
- By{Name}(string? value)
- By{Name}Not(string? value)
- By{Name}IsNullOrWhiteSpace()
- By{Name}IsNotNullOrWhiteSpace()
- By{Name}Contains(string? value)
- By{Name}DoesNotContain(string? value)
- By{Name}StartsWith(string? value)
- By{Name}DoesNotStartWith(string? value)
- By{Name}EndsWith(string? value)
- By{Name}DoesNotEndWith(string? value)
- By{Name}Like(string? pattern) and By{Name}IsNotLike(string? pattern) - EF Core only. The caller supplies
%and_.
A null or blank string argument is ignored. A null column matches DoesNotContain, DoesNotStartWith, DoesNotEndWith, and IsNotLike.
Enum and Guid
- By{Name}({type}? value)
- By{Name}Not({type}? value)
- By{Name}IsNull() and By{Name}IsNotNull() when the member is nullable
- By{Name}Has({type}? value) and By{Name}HasNot({type}? value) when the enum has
[Flags]
Any comparable member except bool
- By{Plural}(IEnumerable<{type}>? values) - null is ignored, an empty list matches nothing.
StatusbecomesByStatuses. - By{Name}NotIn(IEnumerable<{type}>? values) - null or an empty list is ignored.
IdbecomesByIdNotIn, which does not collide withByNo{Navigation}.
repo.Query<User>()
.ByStatusNot(UserStatus.Disabled)
.ByIdNotIn(ids);
Any type that is nullable (including string)
- By{Name}IsNull()
- By{Name}IsNotNull()
Search
Mark the strings a search box should see with [Searchable]. BySearch ORs Contains across those strings. A null or blank term is ignored.
repo.Query<User>()
.BySearch(term);
OR
ByAny ORs each lambda. A lambda whose By arguments are all null is left out. Select, OrderBy, and Include inside a lambda throw.
repo.Query<User>()
.ByIsDeleted(false)
.ByAny(
user => user.ByName(name),
user => user.ByPhone(phone));
Navigations
By filters. Include loads related rows and does not filter. A null argument to a By method is ignored. A null foreign key fails a positive comparison and is excluded. The query still returns the entity you started with.
Reference navigations (many-to-one, one-to-one, owned types) filter that related instance:
IQueryable<User> users = repo.Query<User>()
.ByName(name)
.ByCompany(company => company.ByActive(true));
await users.ToListAsync();
users.ByIsDeleted(false);
ByActive(true) means user.Company is present and active. The lambda receives IQueryable<Company>, so the usual By methods for Company apply. The result is still IQueryable<User>.
Collection navigations, including one-to-many and many-to-many skip navigations, require one related element to satisfy every By filter in the lambda:
repo.Query<User>()
.ByPosts(post => post.ByTitle("Hello").ByPublished(true));
An explicit many-to-many join is a collection whose lambda filters the join and then the target. Both filters apply to the same assignment, and the rows are still users:
repo.Query<User>()
.ByUserRoles(assignment => assignment
.ByIsPrimary(true)
.ByRole(role => role.ByName("Admin")));
ByNo{Navigation} keeps rows whose related instance is missing or fails the filters. For a collection, no element matches. ByEvery{Navigation} is generated for collections and requires every element to match. By{Navigation}IsNull() and By{Navigation}IsNotNull() test the navigation itself, the same way a nullable column does. An empty collection is not null.
repo.Query<User>()
.ByNoPosts(post => post.ByPublished(true))
.ByEveryPosts(post => post.ByTitleIsNotNull())
.ByCompanyIsNull();
A collection count compares how many related rows there are. A null collection counts as zero. The optional filter uses the same By methods; a null filter, or one that adds no predicate, counts every child.
repo.Query<User>()
.ByPostsCountGreaterThanOrEqual(2, post => post.ByPublished(true));
By{Navigation}{Property}SumGreaterThan, and the same shape for GreaterThanOrEqual, LessThan, and LessThanOrEqual, compare the sum of a numeric child column. Min and Max use the same suffixes. A null or empty collection sums to zero. Min and Max do not match a null or empty collection.
repo.Query<User>()
.ByPostsViewsSumGreaterThan(10);
Sort
OrderBy{Name}, OrderBy{Name}Descending, ThenBy{Name}, and ThenBy{Name}Descending sort the same entity. They do not filter. A reference navigation adds OrderBy{Navigation}{Property} for that related entity's own columns, one level deep. A name that is already an OrderBy on the root entity is left as the root column.
repo.Query<User>()
.ByIsDeleted(false)
.OrderByName()
.ThenByCreatedDescending();
repo.Query<User>()
.OrderByCompanyName();
SkipRows(int? count) and TakeRows(int? count) page without taking the names Skip and Take. Null or a negative count leaves the query unchanged. Zero is passed through, so TakeRows(0) is empty.
repo.Query<User>()
.OrderByName()
.SkipRows(offset)
.TakeRows(limit);
Eager load
Include{Navigation}() loads the related rows onto the entity you already filtered, and it does not remove any rows. On EF Core, the next Include is ThenInclude, so IncludeOffice() loads the office that belongs to the company already included.
repo.Query<User>()
.ByCompany(company => company.ByActive(true))
.IncludeCompany()
.IncludeOffice();
/// <summary>
/// Interface for interacting with data storage through a queryable repository pattern
/// </summary>
public interface IRepository : IDisposable
{
/// <summary>Event that fires when an item is added</summary>
event Action<object> ItemAdding;
/// <summary>Event that fires when an itemis modified</summary>
event Action<object> ItemModifing;
/// <summary>Event that fires when an item is deleted</summary>
event Action<object> ItemDeleting;
/// <summary>Queriable Entity</summary>
IQueryable<TEntity> Query<TEntity>() where TEntity : class, new();
/// <summary>
/// Find an entity based on key(s)
/// </summary>
/// <param name="keys">The key(s) for the table</param>
/// <returns>Entity if found, otherwise null</returns>
TEntity FindOne<TEntity>(params object[] keys) where TEntity : class, new();
/// <summary>
/// Find an entity based on key(s)
/// </summary>
/// <param name="keys">The key(s) for the table</param>
/// <returns>Entity if found, otherwise null</returns>
Task<TEntity> FindOneAsync<TEntity>(params object[] keys) where TEntity : class, new();
/// <summary>
/// Adds entities explicily, even if a key is present
/// </summary>
/// <param name="values">Entities to add</param>
void AddNew<TEntity>(params TEntity[] values) where TEntity : class, new();
/// <summary>
/// Add or update entities
/// </summary>
/// <remarks>
/// If the key field of the entity is populated with a non-default value, the framework
/// will assume that the entity is being updated.
/// </remarks>
/// <param name="values">Entities to add</param>
void AddOrUpdate<TEntity>(params TEntity[] values) where TEntity : class, new();
/// <summary>
/// Add or update entities
/// </summary>
/// <param name="collection">Entities to add</param>
void AddOrUpdate<TEntity>(IEnumerable<TEntity> collection) where TEntity : class, new();
/// <summary>
/// Delete a single entity by key(s)
/// </summary>
/// <param name="keys">The key(s) for the table</param>
void DeleteOne<TEntity>(params object[] keys) where TEntity : class, new();
/// <summary>
/// Delete one or more entities
/// </summary>
/// <param name="values">Entities to delete</param>
void Delete<TEntity>(params TEntity[] values) where TEntity : class, new();
/// <summary>
/// Delete one or more entities
/// </summary>
/// <param name="collection">Entities to delete</param>
void Delete<TEntity>(IEnumerable<TEntity> collection) where TEntity : class, new();
/// <summary>
/// Save pending changes for the collection
/// </summary>
/// <returns>Number of affected entities</returns>
int Save();
/// <summary>
/// Save pending changes for the collection async with cancellation
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Number of affected entities</returns>
Task<int> SaveAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Begins a transaction on this repository's store session.
/// </summary>
IRepositoryTransaction BeginTransaction(RepositoryIsolation isolation = RepositoryIsolation.Default);
/// <summary>
/// Begins a transaction on this repository's store session.
/// </summary>
Task<IRepositoryTransaction> BeginTransactionAsync(
RepositoryIsolation isolation = RepositoryIsolation.Default,
CancellationToken cancellationToken = default);
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 is compatible. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 is compatible. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net452 is compatible. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETCoreApp 3.0
- Microsoft.EntityFrameworkCore (>= 3.1.22)
- Microsoft.EntityFrameworkCore.Relational (>= 3.1.22)
-
.NETFramework 4.5.2
- EntityFramework (>= 6.5.1)
-
.NETStandard 2.0
- Microsoft.EntityFrameworkCore (>= 2.2.6)
- Microsoft.EntityFrameworkCore.Relational (>= 2.2.6)
-
.NETStandard 2.1
- Microsoft.EntityFrameworkCore (>= 3.1.22)
- Microsoft.EntityFrameworkCore.Relational (>= 3.1.22)
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
-
net5.0
- Microsoft.EntityFrameworkCore (>= 5.0.13)
- Microsoft.EntityFrameworkCore.Relational (>= 5.0.13)
-
net6.0
- Microsoft.EntityFrameworkCore (>= 6.0.1)
- Microsoft.EntityFrameworkCore.Relational (>= 6.0.1)
- Microsoft.Extensions.Caching.Memory (>= 6.0.2)
-
net7.0
- Microsoft.EntityFrameworkCore (>= 7.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 7.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 85 | 11/3/2026 |
| 2.5.5 | 448 | 11/25/2025 |
| 2.5.0 | 4,476 | 2/21/2025 |
| 2.3.5 | 2,493 | 5/20/2022 |
| 2.2.0 | 1,067 | 1/13/2022 |
| 2.1.1 | 1,055 | 1/12/2022 |
| 2.0.0 | 889 | 1/11/2022 |
| 2.0.0-alpha | 1,147 | 10/7/2020 |
| 1.1.3 | 1,313 | 11/27/2019 |
| 1.1.1 | 1,493 | 11/13/2019 |
| 1.0.2 | 2,218 | 7/21/2017 |
| 1.0.1 | 1,875 | 6/19/2017 |
| 1.0.0-alpha | 2,075 | 5/23/2017 |
Updated the interface to allow for using a single repository objects for multiple different types.
Updated to include a source generator that creates helper methods for filtering by object properties.