{"id":36464450,"url":"https://github.com/gnaeus/EntityFramework.CommonTools","last_synced_at":"2026-01-18T17:00:44.166Z","repository":{"id":60773977,"uuid":"97835343","full_name":"gnaeus/EntityFramework.CommonTools","owner":"gnaeus","description":"Extensions, Auditing, Concurrency Checks, JSON properties and Transaction Logs for EntityFramework and EFCore","archived":false,"fork":false,"pushed_at":"2019-07-26T16:18:41.000Z","size":171,"stargazers_count":108,"open_issues_count":4,"forks_count":18,"subscribers_count":11,"default_branch":"master","last_synced_at":"2026-01-14T06:03:22.415Z","etag":null,"topics":["auditing","change-tracker","complex-types","concurrency-checks","ef-core","ef6","efcore","entity-framework","entity-framework-core","entityframework","entityframeworkcore","extension-methods","json","specification","specification-pattern","transaction-log"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gnaeus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-07-20T12:56:40.000Z","updated_at":"2025-05-13T17:19:35.000Z","dependencies_parsed_at":"2022-10-04T15:37:00.801Z","dependency_job_id":null,"html_url":"https://github.com/gnaeus/EntityFramework.CommonTools","commit_stats":null,"previous_names":["gnaeus/entityframework.changetrackingextensions"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/gnaeus/EntityFramework.CommonTools","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnaeus%2FEntityFramework.CommonTools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnaeus%2FEntityFramework.CommonTools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnaeus%2FEntityFramework.CommonTools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnaeus%2FEntityFramework.CommonTools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gnaeus","download_url":"https://codeload.github.com/gnaeus/EntityFramework.CommonTools/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gnaeus%2FEntityFramework.CommonTools/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28543492,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T14:59:57.589Z","status":"ssl_error","status_checked_at":"2026-01-18T14:59:46.540Z","response_time":98,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auditing","change-tracker","complex-types","concurrency-checks","ef-core","ef6","efcore","entity-framework","entity-framework-core","entityframework","entityframeworkcore","extension-methods","json","specification","specification-pattern","transaction-log"],"created_at":"2026-01-12T00:00:48.965Z","updated_at":"2026-01-18T17:00:44.159Z","avatar_url":"https://github.com/gnaeus.png","language":"C#","readme":"# EntityFramework.CommonTools \u003cimg alt=\"logo\" src=\"icon.png\" width=\"128\" height=\"128\" align=\"right\" /\u003e\nExtension for EntityFramework and EntityFramework Core that provides: Expandable Extension Methods, Complex Types as JSON, Auditing, Concurrency Checks, Specifications and serializable Transacton Logs.\n\n[![Build status](https://ci.appveyor.com/api/projects/status/85f7aqrh2plkl7yn?svg=true)](https://ci.appveyor.com/project/gnaeus/entityframework-commontools)\n[![codecov](https://codecov.io/gh/gnaeus/EntityFramework.CommonTools/branch/master/graph/badge.svg)](https://codecov.io/gh/gnaeus/EntityFramework.CommonTools)\n[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/gnaeus/EntityFramework.CommonTools/master/LICENSE)\n[![NuGet version](https://img.shields.io/nuget/v/EntityFramework.CommonTools.svg)](https://www.nuget.org/packages/EntityFramework.CommonTools)\n[![NuGet version](https://img.shields.io/nuget/v/EntityFrameworkCore.CommonTools.svg)](https://www.nuget.org/packages/EntityFrameworkCore.CommonTools)\n\n## Documentation\n * [Expandable IQueryable Extensions](#ef-querying)\n * [JSON Complex Types](#ef-json-field)\n * [Specification Pattern](#ef-specification)\n * [Auditable Entities](#ef-auditable-entities)\n * [Concurrency Checks](#ef-concurrency-checks)\n * [Transaction Logs](#ef-transaction-logs)\n * [DbContext Extensions (EF 6 only)](#ef-6-only)\n * [Usage with EntityFramework Core](#ef-core-usage)\n * [Usage with EntityFramework 6](#ef-6-usage)\n * [Changelog](#changelog)\n\n### NuGet\n```\nPM\u003e Install-Package EntityFramework.CommonTools\n\nPM\u003e Install-Package EntityFrameworkCore.CommonTools\n```\n\n\u003cbr\u003e\n\n## Attaching ExpressionVisitor to IQueryable\n\nWith `.AsVisitable()` extension we can attach any `ExpressionVisitor` to `IQueryable\u003cT\u003e`.\n\n```cs\npublic static IQueryable\u003cT\u003e AsVisitable\u003cT\u003e(\n    this IQueryable\u003cT\u003e queryable, params ExpressionVisitor[] visitors);\n```\n\n## \u003ca name=\"ef-querying\"\u003e\u003c/a\u003e Expandable extension methods for IQueryable\n\nWe can use extension methods for `IQueryable\u003cT\u003e` to incapsulate custom buisiness logic.  \nBut if we call these methods from `Expression\u003cTDelegate\u003e`, we get runtime error.\n\n```cs\npublic static IQueryable\u003cPost\u003e FilterByAuthor(this IQueryable\u003cPost\u003e posts, int authorId)\n{\n    return posts.Where(p =\u003e p.AuthorId = authorId);\n}\n\npublic static IQueryable\u003cComment\u003e FilterTodayComments(this IQueryable\u003cComment\u003e comments)\n{\n    DateTime today = DateTime.Now.Date;\n\n    return comments.Where(c =\u003e c.CreationTime \u003e today)\n}\n\nComment[] comments = context.Posts\n    .FilterByAuthor(authorId)   // it's OK\n    .SelectMany(p =\u003e p.Comments\n        .AsQueryable()\n        .FilterTodayComments()) // will throw Error\n    .ToArray();\n```\n\nWith `.AsExpandable()` extension we can use extension methods everywhere.\n\n```cs\nComment[] comments = context.Posts\n    .AsExpandable()\n    .FilterByAuthor(authorId)   // it's OK\n    .SelectMany(p =\u003e p.Comments\n        .FilterTodayComments()) // it's OK too\n    .ToArray();\n```\n\nExpandable extension methods should return `IQueryable` and should have `[Expandable]` attribute.\n\n```cs\n[Expandable]\npublic static IQueryable\u003cPost\u003e FilterByAuthor(this IEnumerable\u003cPost\u003e posts, int authorId)\n{\n    return posts.AsQueryable().Where(p =\u003e p.AuthorId = authorId);\n}\n\n[Expandable]\npublic static IQueryable\u003cComment\u003e FilterTodayComments(this IEnumerable\u003cComment\u003e comments)\n{\n    DateTime today = DateTime.Now.Date;\n\n    return comments.AsQueryable().Where(c =\u003e c.CreationTime \u003e today)\n}\n```\n\n### [Benchmarks](./EFCore.CommonTools.Benchmarks/Querying/DatabaseQueryBenchmark.cs)\n```\n          Method |        Median |     StdDev | Scaled | Scaled-SD |\n---------------- |-------------- |----------- |------- |---------- |\n        RawQuery |   555.6202 μs | 15.1837 μs |   1.00 |      0.00 |\n ExpandableQuery |   644.6258 μs |  3.7793 μs |   1.15 |      0.03 | \u003c\u003c\u003c\n  NotCachedQuery | 2,277.7138 μs | 10.9754 μs |   4.06 |      0.10 |\n```\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-json-field\"\u003e\u003c/a\u003e JSON Complex Types\nThere is an utility struct named `JsonField`, that helps to persist any Complex Type as JSON string in single table column.\n\n```cs\nstruct JsonField\u003cTObject\u003e\n    where TObject : class\n{\n    public string Json { get; set; }\n    public TObject Object { get; set; }\n}\n```\n\nUsage:\n```cs\nclass User\n{\n    public int Id { get; set; }\n    public string Name { get; set; }\n    public string Login { get; set; }\n\n    private JsonField\u003cAddress\u003e _address;\n    // used by EntityFramework\n    public string AddressJson\n    {\n        get { return _address.Json; }\n        set { _address.Json = value; }\n    }\n    // used by application code\n    public Address Address\n    {\n        get { return _address.Object; }\n        set { _address.Object = value; }\n    }\n\n    // collection initialization by default\n    private JsonField\u003cICollection\u003cstring\u003e\u003e _phones = new HashSet\u003cstring\u003e();\n    public string PhonesJson\n    {\n        get { return _phones.Json; }\n        set { _phones.Json = value; }\n    }\n    public ICollection\u003cstring\u003e Phones\n    {\n        get { return _phones.Object; }\n        set { _phones.Object = value; }\n    }\n}\n\n[NotMapped]\nclass Address\n{\n    public string City { get; set; }\n    public string Street { get; set; }\n    public string Building { get; set; }\n}\n```\n\nIf we update these Complex Type properties, the following SQL is generated during `SaveChanges`:\n```sql\nUPDATE Users\nSET AddressJson = '{\"City\":\"Moscow\",\"Street\":\"Arbat\",\"Building\":\"10\"}',\n    PhonesJson = '[\"+7 (123) 456-7890\",\"+7 (098) 765-4321\"]'\nWHERE Id = 1;\n```\n\nThe `AddressJson` property is serialized from `Address` only when it accessed by EntityFramework.  \nAnd the `Address` property is materialized from `AddressJson` only when EntityFramework writes to `AddressJson`.\n\nIf we want to initialize some JSON collection in entity consctuctor, for example:\n```cs\nclass MyEntity\n{\n    public ICollection\u003cMyObject\u003e MyObjects { get; set; } = new HashSet\u003cMyObject\u003e();\n}\n```\nWe can use the following implicit conversion:\n```cs\nclass MyEntity\n{\n    private JsonField\u003cICollection\u003cMyObject\u003e\u003e _myObjects = new HashSet\u003cMyObject\u003e();\n}\n```\nIt uses the following implicit operator:\n```cs\nstruct JsonField\u003cTObject\u003e\n{\n    public static implicit operator JsonField\u003cTObject\u003e(TObject defaultValue);\n}\n```\n\nThe only caveat is that `TObject` object should not contain reference loops.  \nBecause `JsonField` uses [Jil](https://github.com/kevin-montrose/Jil) (the fastest .NET JSON serializer) behind the scenes.\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-specification\"\u003e\u003c/a\u003e Specification Pattern\n\nGeneric implementation of [Specification Pattern](https://en.wikipedia.org/wiki/Specification_pattern).\n\n```cs\npublic interface ISpecification\u003cT\u003e\n{\n    bool IsSatisfiedBy(T entity);\n\n    Expression\u003cFunc\u003cT, bool\u003e\u003e ToExpression();\n}\n\npublic class Specification\u003cT\u003e : ISpecification\u003cT\u003e\n{\n    public Specification(Expression\u003cFunc\u003cT, bool\u003e\u003e predicate);\n}\n```\n\nWe can define named specifications:\n```cs\nclass UserIsActiveSpec : Specification\u003cUser\u003e\n{\n    public UserIsActiveSpec()\n        : base(u =\u003e !u.IsDeleted) { }\n}\n\nclass UserByLoginSpec : Specification\u003cUser\u003e\n{\n    public UserByLoginSpec(string login)\n        : base(u =\u003e u.Login == login) { }\n}\n```\n\nThen we can combine specifications with conditional logic operators `\u0026\u0026`, `||` and `!`:\n```cs\nclass CombinedSpec\n{\n    public CombinedSpec(string login)\n        : base(new UserIsActiveSpec() \u0026\u0026 new UserByLoginSpec(login)) { }\n}\n```\n\nAlso we can test it:\n```cs\nvar user = new User { Login = \"admin\", IsDeleted = false };\nvar spec = new CombinedSpec(\"admin\");\n\nAssert.IsTrue(spec.IsSatisfiedBy(user));\n```\n\nAnd use with `IEnumerable\u003cT\u003e`:\n\n```cs\nvar users = Enumerable.Empty\u003cUser\u003e();\nvar spec = new UserByLoginSpec(\"admin\");\n\nvar admin = users.FirstOrDefault(spec.IsSatisfiedBy);\n\n// or even\nvar admin = users.FirstOrDefault(spec);\n```\n\nOr even with `IQueryable\u003cT\u003e`:\n```cs\nvar spec = new UserByLoginSpec(\"admin\");\n\nvar admin = context.Users.FirstOrDefault(spec.ToExpression());\n\n// or even\nvar admin = context.Users.FirstOrDefault(spec);\n\n// and also inside Expression\nvar adminFiends = context.Users\n    .AsVisitable(new SpecificationExpander())\n    .Where(u =\u003e u.Firends.Any(spec.ToExpression()))\n    .ToList();\n\n// or even\nvar adminFiends = context.Users\n    .AsVisitable(new SpecificationExpander())\n    .Where(u =\u003e u.Firends.Any(spec))\n    .ToList();\n```\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-auditable-entities\"\u003e\u003c/a\u003e Auditable Entities\nAutomatically update info about who and when create / modify / delete the entity during `context.SaveCahnges()`\n\n```cs\nclass User\n{\n    public int Id { get;set; }\n    public string Login { get; set; }\n}\n\nclass Post : IFullAuditable\u003cint\u003e\n{\n    public int Id { get; set; }\n    public string Content { get; set; }\n    \n    // IFullAuditable\u003cint\u003e members\n    public bool IsDeleted { get; set; }\n    public int CreatorUserId { get; set; }\n    public DateTime CreatedUtc { get; set; }\n    public int? UpdaterUserId { get; set; }\n    public DateTime? UpdatedUtc { get; set; }\n    public int? DeleterUserId { get; set; }\n    public DateTime? DeletedUtc { get; set; }\n}\n\nclass MyContext : DbContext\n{\n    public DbSet\u003cUser\u003e Users { get; set; }\n    public DbSet\u003cPost\u003e Posts { get; set; }\n\n    public void SaveChanges(int editorUserId)\n    {\n        this.UpdateAuditableEntities(editorUserId);\n        base.SaveChanges();\n    }\n}\n```\n\n\u003cbr\u003e\n\nAlso you can track only the creation, deletion and so on by implementing the following interfaces:\n\n#### `ISoftDeletable`\nUsed to standardize soft deleting entities. Soft-delete entities are not actually deleted,\nmarked as `IsDeleted == true` in the database, but can not be retrieved to the application.\n\n```cs\ninterface ISoftDeletable\n{\n    bool IsDeleted { get; set; }\n}\n```\n\n#### `ICreationTrackable`\nAn entity can implement this interface if `CreatedUtc` of this entity must be stored.\n`CreatedUtc` is automatically set when saving Entity to database.\n\n```cs\ninterface ICreationTrackable\n{\n    DateTime CreatedUtc { get; set; }\n}\n```\n\n#### `ICreationAuditable\u003cTUserId\u003e`\nThis interface is implemented by entities that is wanted to store creation information (who and when created).\nCreation time and creator user are automatically set when saving Entity to database.\n\n```cs\ninterface ICreationAuditable\u003cTUserId\u003e : ICreationTrackable\n    where TUserId : struct\n{\n    TUserId CreatorUserId { get; set; }\n}\n// or\ninterface ICreationAuditable : ICreationTrackable\n{\n    string CreatorUserId { get; set; }\n}\n```\n\n#### `IModificationTrackable`\nAn entity can implement this interface if `UpdatedUtc` of this entity must be stored.\n`UpdatedUtc` automatically set when updating the Entity.\n\n```cs\ninterface IModificationTrackable\n{\n    DateTime? UpdatedUtc { get; set; }\n}\n```\n\n#### `IModificationAuditable\u003cTUserId\u003e`\nThis interface is implemented by entities that is wanted\nto store modification information (who and when modified lastly).\nProperties are automatically set when updating the Entity.\n\n```cs\ninterface IModificationAuditable\u003cTUserId\u003e : IModificationTrackable\n    where TUserId : struct\n{\n    TUserId? UpdaterUserId { get; set; }\n}\n// or\ninterface IModificationAuditable : IModificationTrackable\n{\n    string UpdaterUserId { get; set; }\n}\n```\n\n#### `IDeletionTrackable`\nAn entity can implement this interface if `DeletedUtc` of this entity must be stored.\n`DeletedUtc` is automatically set when deleting Entity.\n\n```cs\ninterface IDeletionTrackable : ISoftDeletable\n{\n    DateTime? DeletedUtc { get; set; }\n}\n```\n\n#### `IDeletionAuditable\u003cTUserId\u003e`\nThis interface is implemented by entities which wanted to store deletion information (who and when deleted).\n\n```cs\npublic interface IDeletionAuditable\u003cTUserId\u003e : IDeletionTrackable\n    where TUserId : struct\n{\n    TUserId? DeleterUserId { get; set; }\n}\n// or\npublic interface IDeletionAuditable : IDeletionTrackable\n{\n    string DeleterUserId { get; set; }\n}\n```\n\n#### `IFullTrackable`\nThis interface is implemented by entities which modification times must be tracked.\nRelated properties automatically set when saving/updating/deleting Entity objects.\n\n```cs\ninterface IFullTrackable : ICreationTrackable, IModificationTrackable, IDeletionTrackable { }\n```\n\n#### `IFullAuditable\u003cTUserId\u003e`\nThis interface is implemented by entities which must be audited.\nRelated properties automatically set when saving/updating/deleting Entity objects.\n\n```cs\ninterface IFullAuditable\u003cTUserId\u003e : IFullTrackable,\n    ICreationAuditable\u003cTUserId\u003e, IModificationAuditable\u003cTUserId\u003e, IDeletionAuditable\u003cTUserId\u003e\n    where TUserId : struct { }\n// or\ninterface IFullAuditable : IFullTrackable, ICreationAuditable, IModificationAuditable, IDeletionAuditable { }\n```\n\n\u003cbr\u003e\n\nYou can choose between saving the user `Id` or the user `Login`.  \nSo there are two overloadings for `DbContext.UpdateAudiatbleEntities()`:\n```cs\nstatic void UpdateAuditableEntities\u003cTUserId\u003e(this DbContext context, TUserId editorUserId);\nstatic void UpdateAuditableEntities(this DbContext context, string editorUserId);\n```\nand also the separate extension to update only `Trackable` entities:\n```cs\nstatic void UpdateTrackableEntities(this DbContext context);\n```\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-concurrency-checks\"\u003e\u003c/a\u003e Concurrency Checks\nBy default EF and EFCore uses `EntityEntry.OriginalValues[\"RowVersion\"]` for concurrency checks\n([see docs](https://docs.microsoft.com/en-us/ef/core/saving/concurrency)).\n\nWith this behaviour the concurrency conflict may occur only between the `SELECT` statement\nthat loads entities to the `DbContext` and the `UPDATE` statement from `DbContext.SaveChanges()`.\n\nBut sometimes we want check concurrency conflicts between two or more edit operations that comes from client-side. For example:\n\n* user_1 loads the editor form\n* user_2 loads the same editor form\n* user_1 saves his changes\n* user_2 saves his changes __and gets concurrency conflict__.\n\nTo provide this behaviour, an entity should implement the following interface:\n```cs\ninterface IConcurrencyCheckable\u003cTRowVersion\u003e\n{\n    TRowVersion RowVersion { get; set; }\n}\n```\nAnd the `DbContext` should overload `SaveChanges()` method with `UpdateConcurrentEntities()` extension:\n```cs\nclass MyDbContext : DbContext\n{\n    public override int SaveChanges()\n    {\n        this.UpdateConcurrentEntities();\n        return base.SaveChanges();\n    }\n}\n```\n\n\u003cbr\u003e\n\nThere are also three different behaviours for `IConcurrencyCheckable\u003cT\u003e`:\n\n#### `IConcurrencyCheckable\u003cbyte[]\u003e`\n`RowVersion` property should be decorated by `[Timestamp]` attribute.  \n`RowVersion` column should have `ROWVERSION` type in SQL Server.  \nThe default behaviour. Supported only by Microsoft SQL Server.\n\n```cs\nclass MyEntity : IConcurrencyCheckable\u003cGuid\u003e\n{\n    [Timestamp]\n    public byte[] RowVersion { get; set; }\n}\n```\n\n#### `IConcurrencyCheckable\u003cGuid\u003e`\n`RowVersion` property should be decorated by `[ConcurrencyCheck]` attribute.  \nIt's value is populated by `Guid.NewGuid()` during each `DbContext.SaveChanges()` call at client-side.  \nNo specific database support is needed.\n\n```cs\nclass MyEntity : IConcurrencyCheckable\u003cGuid\u003e\n{\n    [ConcurrencyCheck]\n    public Guid RowVersion { get; set; }\n}\n```\n\n#### `IConcurrencyCheckable\u003clong\u003e`\n`RowVersion` property should be decorated by `[ConcurrencyCheck]` and `[DatabaseGenerated(DatabaseGeneratedOption.Computed)]` attributes.\n\n```cs\nclass MyEntity : IConcurrencyCheckable\u003clong\u003e\n{\n    [ConcurrencyCheck]\n    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]\n    public long RowVersion { get; set; }\n}\n```\n\n`RowVersion` column should be updated by trigger in DB. Example for SQLite:\n```sql\nCREATE TABLE MyEntities ( RowVersion INTEGER DEFAULT 0 );\n\nCREATE TRIGGER TRG_MyEntities_UPD\nAFTER UPDATE ON MyEntities\n    WHEN old.RowVersion = new.RowVersion\nBEGIN\n    UPDATE MyEntities\n    SET RowVersion = RowVersion + 1;\nEND;\n```\n\n\u003cbr\u003e\n\nBut sometimes we want to ignore `DbUpdateConcurrencyException`.\nAnd there are two extension methods for this.\n\n__`static void SaveChangesIgnoreConcurrency(this DbContext dbContext, int retryCount = 3)`__  \nSave changes regardless of `DbUpdateConcurrencyException`.\n\n__`static async Task SaveChangesIgnoreConcurrencyAsync(this DbContext dbContext, int retryCount = 3)`__  \nSave changes regardless of `DbUpdateConcurrencyException`.\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-transaction-logs\"\u003e\u003c/a\u003e Transaction Logs\nWrite all inserted / updated / deleted entities (serialized to JSON) to the separete table named `TransactionLog`.\n\nTo capture transaction logs an entity must inherit from empty `ITransactionLoggable { }` interface.\n\nAnd the `DbContext` should overload `SaveChanges()` method with `SaveChangesWithTransactionLog()` wrapper,  \nand register the `TransactionLog` entity in `ModelBuilder`.\n\n```cs\nclass Post : ITransactionLoggable\n{\n    public string Content { get; set; }\n}\n\n// for EntityFramework 6\nclass MyDbContext : DbContext\n{\n    public DbSet\u003cPost\u003e Posts { get; set; }\n\n    protected override void OnModelCreating(DbModelBuilder modelBuilder)\n    {\n        modelBuilder.UseTransactionLog();\n    }\n\n    public override int SaveChanges()\n    {\n        return this.SaveChangesWithTransactionLog(base.SaveChanges);\n    }\n\n    // override the most general SaveChangesAsync\n    public override Task\u003cint\u003e SaveChangesAsync(CancellationToken cancellationToken)\n    {\n        return this.SaveChangesWithTransactionLogAsync(base.SaveChangesAsync, cancellationToken);\n    }\n}\n\n// for EntityFramework Core\nclass MyCoreDbContext : DbContext\n{\n    public DbSet\u003cPost\u003e Posts { get; set; }\n\n    protected override void OnModelCreating(DbModelBuilder modelBuilder)\n    {\n        modelBuilder.UseTransactionLog();\n    }\n\n    // override the most general SaveChanges\n    public override int SaveChanges(bool acceptAllChangesOnSuccess)\n    {\n        return this.SaveChangesWithTransactionLog(base.SaveChanges, acceptAllChangesOnSuccess);\n    }\n\n    // override the most general SaveChangesAsync\n    public override Task\u003cint\u003e SaveChangesAsync(\n        bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        return this.SaveChangesWithTransactionLogAsync(\n            base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken);\n    }\n}\n\n```\n\nAfter that the transaction logs can be accessed via `TransactionLog` entity:\n\n```cs\nclass TransactionLog\n{\n    // Auto incremented primary key.\n    public long Id { get; set; }\n    \n    // An ID of all changes that captured during single DbContext.SaveChanges() call.\n    public Guid TransactionId { get; set; }\n\n    // UTC timestamp of DbContext.SaveChanges() call.\n    public DateTime CreatedUtc { get; set; }\n\n    // \"INS\", \"UPD\" or \"DEL\". Not null.\n    public string Operation { get; set; }\n\n    // Schema for captured entity. Can be null for SQLite.\n    public string Schema { get; set; }\n\n    // Table for captured entity. Not null.\n    public string TableName { get; set; }\n\n    // Assembly qualified type name of captured entity. Not null.\n    public string EntityType { get; set; }\n\n    // The captured entity serialized to JSON by Jil serializer. Not null.\n    public string EntityJson { get; set; }\n\n    // Lazily deserialized entity object.\n    // Type for deserialization is taken from EntityType property.\n    // All navigation properties and collections will be empty.\n    public object Entity { get; }\n\n    // Get strongly typed entity from transaction log.\n    // Can be null if TEntity and type from EntityType property are incompatible.\n    // All navigation properties and collections will be empty.\n    public TEntity GetEntity\u003cTEntity\u003e();\n}\n```\n\nFor querying and indexing TransactionLogs table see [#2](https://github.com/gnaeus/EntityFramework.CommonTools/issues/2).\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-6-only\"\u003e\u003c/a\u003e DbContext Extensions (EF 6 only)\n\n__`static IDisposable WithoutChangeTracking(this DbContext dbContext)`__  \nDisposable token for `using(...)` statement where `DbContext.Configuration.AutoDetectChanges` is disabled.\n\n```cs\n// here AutoDetectChanges is enabled\nusing (dbContext.WithoutChangeTracking())\n{\n    // inside this block AutoDetectChanges is disabled\n}\n// here AutoDetectChanges is enabled again\n```\n\n\u003cbr\u003e\n\n__`static IDisposable WithChangeTrackingOnce(this DbContext dbContext)`__  \nRun `DbChangeTracker.DetectChanges()` once and return disposable token for `using(...)` statement\nwhere `DbContext.Configuration.AutoDetectChanges` is disabled.\n\n```cs\n// here AutoDetectChanges is enabled\nusing (dbContext.WithChangeTrackingOnce())\n{\n    // inside this block AutoDetectChanges is disabled\n}\n// here AutoDetectChanges is enabled again\n```\n\n\u003cbr\u003e\n\n__`static TableAndSchema GetTableAndSchemaName(this DbContext context, Type entityType)`__  \nGet corresponding table name and schema by `entityType`.\n\n__`static TableAndSchema[] GetTableAndSchemaNames(this DbContext context, Type entityType)`__  \nGet corresponding table name and schema by `entityType`.\nUse it if entity is splitted between multiple tables.\n\n```cs\nstruct TableAndSchema\n{\n    public string TableName;\n    public string Schema;\n}\n```\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-core-usage\"\u003e\u003c/a\u003e All together example for EntityFramework Core\n```cs\nclass MyDbContext : DbContext\n{\n    protected override void OnModelCreating(DbModelBuilder modelBuilder)\n    {\n        modelBuilder.UseTransactionLog();\n    }\n\n    // override the most general SaveChanges\n    public override int SaveChanges(bool acceptAllChangesOnSuccess)\n    {\n        this.UpdateTrackableEntities();\n        this.UpdateConcurrentEntities();\n\n        return this.SaveChangesWithTransactionLog(base.SaveChanges, acceptAllChangesOnSuccess);\n    }\n\n    // override the most general SaveChangesAsync\n    public override Task\u003cint\u003e SaveChangesAsync(\n        bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))\n    {\n        this.UpdateTrackableEntities();\n        this.UpdateConcurrentEntities();\n\n        return this.SaveChangesWithTransactionLogAsync(\n            base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken);\n    }\n\n    public int SaveChanges(int editorUserId)\n    {\n        this.UpdateAuditableEntities(editorUserId);\n        \n        return SaveChanges();\n    }\n\n    public Task\u003cint\u003e SaveChangesAsync(int editorUserId)\n    {\n        this.UpdateAuditableEntities(editorUserId);\n\n        return SaveChangesAsync();\n    }\n}\n```\n\n\u003cbr\u003e\n\n## \u003ca name=\"ef-6-usage\"\u003e\u003c/a\u003e All together example for EntityFramework 6\n```cs\nclass MyDbContext : DbContext\n{\n    protected override void OnModelCreating(DbModelBuilder modelBuilder)\n    {\n        modelBuilder.UseTransactionLog();\n    }\n\n    // override the most general SaveChanges\n    public override int SaveChanges()\n    {\n        using (this.WithChangeTrackingOnce())\n        {\n            this.UpdateTrackableEntities();\n            this.UpdateConcurrentEntities();\n\n            return this.SaveChangesWithTransactionLog(base.SaveChanges);\n        }\n    }\n\n    // override the most general SaveChangesAsync\n    public override Task\u003cint\u003e SaveChangesAsync(CancellationToken cancellationToken)\n    {\n        using (this.WithChangeTrackingOnce())\n        {\n            this.UpdateTrackableEntities();\n            this.UpdateConcurrentEntities();\n\n            return this.SaveChangesWithTransactionLogAsync(base.SaveChangesAsync, cancellationToken);\n        }\n    }\n\n    public int SaveChanges(int editorUserId)\n    {\n        this.UpdateAuditableEntities(editorUserId);\n        \n        return SaveChanges();\n    }\n\n    public Task\u003cint\u003e SaveChangesAsync(int editorUserId)\n    {\n        this.UpdateAuditableEntities(editorUserId);\n\n        return SaveChangesAsync();\n    }\n}\n```\n\n\u003chr\u003e\n\n# \u003ca name=\"changelog\"\u003e\u003c/a\u003e Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/)\nand this project adheres to [Semantic Versioning](http://semver.org/).\n\n## [2.0.2] - 2018-05-10\n### Fixed\n- `context.Update(entity)` does not reset `ICreationTrackable.CreatedUtc`\n  and `ICreationAuditable.CreatorUserId` to empty values [#4](https://github.com/gnaeus/EntityFramework.CommonTools/issues/4)\n\n## [2.0.1] - 2018-03-27\n### Fixed\n- `.AsExpandable()` works with bound lambda arguments\n\n## [2.0.0] - 2018-03-23\n### Added\n- EFCore 2.0 support\n- EntityFramework 6.2 support\n\n### Changed\n- `ICreationAuditable.CreatorUser` renamed to `CreatorUserId`\n- `IModificationAuditable.UpdaterUser` renamed to `UpdaterUserId`\n- `IDeletionAuditable.DeleterUser` renamed to `DeleterUserId`\n\nSee [#1](https://github.com/gnaeus/EntityFramework.CommonTools/issues/1).\n\nFor compatibility issues you still can use these interfaces:\n```cs\npublic interface ICreationAuditableV1\n{\n    string CreatorUser { get; set; }\n}\n\npublic interface IModificationAuditableV1\n{\n    string UpdaterUser { get; set; }\n}\n\npublic interface IDeletionAuditableV1\n{\n    string DeleterUser { get; set; }\n}\n\npublic interface IFullAuditableV1 : IFullTrackable,\n    ICreationAuditableV1, IModificationAuditableV1, IDeletionAuditableV1\n{\n}\n```\n\n## [1.0.0] - 2017-08-16\n### Added\nInitial project version.\n\n[2.0.2]: https://github.com/gnaeus/EntityFramework.CommonTools/compare/2.0.1...2.0.2\n[2.0.1]: https://github.com/gnaeus/EntityFramework.CommonTools/compare/2.0.0...2.0.1\n[2.0.0]: https://github.com/gnaeus/EntityFramework.CommonTools/compare/1.0.0...2.0.0\n[1.0.0]: https://github.com/gnaeus/EntityFramework.CommonTools/tree/1.0.0\n","funding_links":[],"categories":["Unsupported Packages"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgnaeus%2FEntityFramework.CommonTools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgnaeus%2FEntityFramework.CommonTools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgnaeus%2FEntityFramework.CommonTools/lists"}