Mindfire.EFRepository 3.0.0

dotnet add package Mindfire.EFRepository --version 3.0.0
                    
NuGet\Install-Package Mindfire.EFRepository -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mindfire.EFRepository" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mindfire.EFRepository" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Mindfire.EFRepository" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mindfire.EFRepository --version 3.0.0
                    
#r "nuget: Mindfire.EFRepository, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mindfire.EFRepository@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mindfire.EFRepository&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Mindfire.EFRepository&version=3.0.0
                    
Install as a Cake Tool

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 By E.g.
var query = Repo.Query<Users>()
	.ByUsernameStartsWith("Bill")
	.ByIsDeleted(false)
	.ByRegistrationDateIsBefore(startDate)
	.AsNoTracking();
  • Decoupling Data Context from Code for Better Testability via the IRepository interface
  • C# 10 nullable compatible 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
  • async and synchronous methods provided where possible. Chaining to LINQ's async methods is strongly advised (like .CountAsync()).
  • Store-neutral transactions through BeginTransaction and InTransactionAsync

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) - DateTime and DateTimeOffset only. 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. Status becomes ByStatuses.
  • By{Name}NotIn(IEnumerable<{type}>? values) - null or an empty list is ignored. Id becomes ByIdNotIn, which does not collide with ByNo{Navigation}.
repo.Query<User>()
	.ByStatusNot(UserStatus.Disabled)
	.ByIdNotIn(ids);

Any type that is nullable (including string)

  • By{Name}IsNull()
  • By{Name}IsNotNull()

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));

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.