EntityFramework.CommonTools
Extensions, Auditing, Concurrency Checks, JSON properties and Transaction Logs for EntityFramework and EFCore
Install / Use
/learn @gnaeus/EntityFramework.CommonToolsREADME
EntityFramework.CommonTools <img alt="logo" src="icon.png" width="128" height="128" align="right" />
Extension for EntityFramework and EntityFramework Core that provides: Expandable Extension Methods, Complex Types as JSON, Auditing, Concurrency Checks, Specifications and serializable Transacton Logs.
Documentation
- Expandable IQueryable Extensions
- JSON Complex Types
- Specification Pattern
- Auditable Entities
- Concurrency Checks
- Transaction Logs
- DbContext Extensions (EF 6 only)
- Usage with EntityFramework Core
- Usage with EntityFramework 6
- Changelog
NuGet
PM> Install-Package EntityFramework.CommonTools
PM> Install-Package EntityFrameworkCore.CommonTools
<br>
Attaching ExpressionVisitor to IQueryable
With .AsVisitable() extension we can attach any ExpressionVisitor to IQueryable<T>.
public static IQueryable<T> AsVisitable<T>(
this IQueryable<T> queryable, params ExpressionVisitor[] visitors);
<a name="ef-querying"></a> Expandable extension methods for IQueryable
We can use extension methods for IQueryable<T> to incapsulate custom buisiness logic.
But if we call these methods from Expression<TDelegate>, we get runtime error.
public static IQueryable<Post> FilterByAuthor(this IQueryable<Post> posts, int authorId)
{
return posts.Where(p => p.AuthorId = authorId);
}
public static IQueryable<Comment> FilterTodayComments(this IQueryable<Comment> comments)
{
DateTime today = DateTime.Now.Date;
return comments.Where(c => c.CreationTime > today)
}
Comment[] comments = context.Posts
.FilterByAuthor(authorId) // it's OK
.SelectMany(p => p.Comments
.AsQueryable()
.FilterTodayComments()) // will throw Error
.ToArray();
With .AsExpandable() extension we can use extension methods everywhere.
Comment[] comments = context.Posts
.AsExpandable()
.FilterByAuthor(authorId) // it's OK
.SelectMany(p => p.Comments
.FilterTodayComments()) // it's OK too
.ToArray();
Expandable extension methods should return IQueryable and should have [Expandable] attribute.
[Expandable]
public static IQueryable<Post> FilterByAuthor(this IEnumerable<Post> posts, int authorId)
{
return posts.AsQueryable().Where(p => p.AuthorId = authorId);
}
[Expandable]
public static IQueryable<Comment> FilterTodayComments(this IEnumerable<Comment> comments)
{
DateTime today = DateTime.Now.Date;
return comments.AsQueryable().Where(c => c.CreationTime > today)
}
Benchmarks
Method | Median | StdDev | Scaled | Scaled-SD |
---------------- |-------------- |----------- |------- |---------- |
RawQuery | 555.6202 μs | 15.1837 μs | 1.00 | 0.00 |
ExpandableQuery | 644.6258 μs | 3.7793 μs | 1.15 | 0.03 | <<<
NotCachedQuery | 2,277.7138 μs | 10.9754 μs | 4.06 | 0.10 |
<br>
<a name="ef-json-field"></a> JSON Complex Types
There is an utility struct named JsonField, that helps to persist any Complex Type as JSON string in single table column.
struct JsonField<TObject>
where TObject : class
{
public string Json { get; set; }
public TObject Object { get; set; }
}
Usage:
class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Login { get; set; }
private JsonField<Address> _address;
// used by EntityFramework
public string AddressJson
{
get { return _address.Json; }
set { _address.Json = value; }
}
// used by application code
public Address Address
{
get { return _address.Object; }
set { _address.Object = value; }
}
// collection initialization by default
private JsonField<ICollection<string>> _phones = new HashSet<string>();
public string PhonesJson
{
get { return _phones.Json; }
set { _phones.Json = value; }
}
public ICollection<string> Phones
{
get { return _phones.Object; }
set { _phones.Object = value; }
}
}
[NotMapped]
class Address
{
public string City { get; set; }
public string Street { get; set; }
public string Building { get; set; }
}
If we update these Complex Type properties, the following SQL is generated during SaveChanges:
UPDATE Users
SET AddressJson = '{"City":"Moscow","Street":"Arbat","Building":"10"}',
PhonesJson = '["+7 (123) 456-7890","+7 (098) 765-4321"]'
WHERE Id = 1;
The AddressJson property is serialized from Address only when it accessed by EntityFramework.
And the Address property is materialized from AddressJson only when EntityFramework writes to AddressJson.
If we want to initialize some JSON collection in entity consctuctor, for example:
class MyEntity
{
public ICollection<MyObject> MyObjects { get; set; } = new HashSet<MyObject>();
}
We can use the following implicit conversion:
class MyEntity
{
private JsonField<ICollection<MyObject>> _myObjects = new HashSet<MyObject>();
}
It uses the following implicit operator:
struct JsonField<TObject>
{
public static implicit operator JsonField<TObject>(TObject defaultValue);
}
The only caveat is that TObject object should not contain reference loops.
Because JsonField uses Jil (the fastest .NET JSON serializer) behind the scenes.
<a name="ef-specification"></a> Specification Pattern
Generic implementation of Specification Pattern.
public interface ISpecification<T>
{
bool IsSatisfiedBy(T entity);
Expression<Func<T, bool>> ToExpression();
}
public class Specification<T> : ISpecification<T>
{
public Specification(Expression<Func<T, bool>> predicate);
}
We can define named specifications:
class UserIsActiveSpec : Specification<User>
{
public UserIsActiveSpec()
: base(u => !u.IsDeleted) { }
}
class UserByLoginSpec : Specification<User>
{
public UserByLoginSpec(string login)
: base(u => u.Login == login) { }
}
Then we can combine specifications with conditional logic operators &&, || and !:
class CombinedSpec
{
public CombinedSpec(string login)
: base(new UserIsActiveSpec() && new UserByLoginSpec(login)) { }
}
Also we can test it:
var user = new User { Login = "admin", IsDeleted = false };
var spec = new CombinedSpec("admin");
Assert.IsTrue(spec.IsSatisfiedBy(user));
And use with IEnumerable<T>:
var users = Enumerable.Empty<User>();
var spec = new UserByLoginSpec("admin");
var admin = users.FirstOrDefault(spec.IsSatisfiedBy);
// or even
var admin = users.FirstOrDefault(spec);
Or even with IQueryable<T>:
var spec = new UserByLoginSpec("admin");
var admin = context.Users.FirstOrDefault(spec.ToExpression());
// or even
var admin = context.Users.FirstOrDefault(spec);
// and also inside Expression
var adminFiends = context.Users
.AsVisitable(new SpecificationExpander())
.Where(u => u.Firends.Any(spec.ToExpression()))
.ToList();
// or even
var adminFiends = context.Users
.AsVisitable(new SpecificationExpander())
.Where(u => u.Firends.Any(spec))
.ToList();
<br>
<a name="ef-auditable-entities"></a> Auditable Entities
Automatically update info about who and when create / modify / delete the entity during context.SaveCahnges()
class User
{
public int Id { get;set; }
public string Login { get; set; }
}
class Post : IFullAuditable<int>
{
public int Id { get; set; }
public string Content { get; set; }
// IFullAuditable<int> members
public bool IsDeleted { get; set; }
public int CreatorUserId { get; set; }
public DateTime CreatedUtc { get; set; }
public int? UpdaterUserId { get; set; }
public DateTime? UpdatedUtc { get; set; }
public int? DeleterUserId { get; set; }
public DateTime? DeletedUtc { get; set; }
}
class MyContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Post> Posts { get; set; }
public void SaveChanges(int editorUserId)
{
this.UpdateAuditableEntities(editorUserId);
base.SaveChanges();
}
}
<br>
Also you can track only the creation, deletion and so on by implementing the following interfaces:
ISoftDeletable
Used to standardize soft deleting entities. Soft-delete entities are not actually deleted,
marked as IsDeleted == true in the database, but can not be retrieved to the application.
interface ISoftDeletable
{
bool IsDeleted { get; set; }
}
ICreationTrackable
An entity can implement this interface if CreatedUtc of this entity must be stored.
CreatedUtc is automatically set when saving Entity to database.
interface ICreationTrackable
{
DateTime CreatedUtc { get; set; }
}
`ICreation
Related Skills
ai-cmo
Collection of my Agent Skills and books.
orbit-planning
O.R.B.I.T. - strategic project planning before you build. Objective, Requirements, Blueprint, Implementation Roadmap, Track.
next
A beautifully designed, floating Pomodoro timer that respects your workspace.
product-manager-skills
34PM skill for Claude Code, Codex, Cursor, and Windsurf: diagnose SaaS metrics, critique PRDs, plan roadmaps, run discovery, and coach PM career transitions.
