{"id":37038430,"url":"https://github.com/kemsky/projection-tools","last_synced_at":"2026-01-14T04:34:23.609Z","repository":{"id":178187524,"uuid":"661465266","full_name":"kemsky/projection-tools","owner":"kemsky","description":"Primitives for building reusable LINQ projections and specifications","archived":false,"fork":false,"pushed_at":"2025-09-10T12:19:48.000Z","size":77,"stargazers_count":4,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-01T07:28:19.515Z","etag":null,"topics":["csharp","dotnet","entity-framework","linq","specification-pattern"],"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/kemsky.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-07-02T23:35:49.000Z","updated_at":"2025-09-10T12:19:51.000Z","dependencies_parsed_at":null,"dependency_job_id":"8c9da512-d4ba-476c-9f46-50fbe3a809de","html_url":"https://github.com/kemsky/projection-tools","commit_stats":null,"previous_names":["kemsky/projection-tools"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/kemsky/projection-tools","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemsky%2Fprojection-tools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemsky%2Fprojection-tools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemsky%2Fprojection-tools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemsky%2Fprojection-tools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kemsky","download_url":"https://codeload.github.com/kemsky/projection-tools/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kemsky%2Fprojection-tools/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28409525,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["csharp","dotnet","entity-framework","linq","specification-pattern"],"created_at":"2026-01-14T04:34:22.935Z","updated_at":"2026-01-14T04:34:23.603Z","avatar_url":"https://github.com/kemsky.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build](https://github.com/kemsky/projection-tools/actions/workflows/build.yml/badge.svg)](https://github.com/kemsky/projection-tools/actions/workflows/build.yml)\n\n# Projection Tools\n\nThis package provides primitives for building reusable LINQ projections and specifications.\n\nPackage is available on [Nuget](https://www.nuget.org/packages/ProjectionTools/).\n\nInstall using dotnet CLI:\n```commandline\ndotnet add package ProjectionTools\n```\nInstall using Package-Manager console:\n```commandline\nPM\u003e Install-Package ProjectionTools\n```\n\nI've also published an article on Medium [Alternative specification pattern implementation in C#](https://medium.com/@nimrod97/alternative-specification-pattern-implementation-in-c-f5d88a7ed364).\n\n## Specifications\n\nPredicates can be complex, often a combination of different predicates depending on business logic.\n\nThere is a well-known specification pattern and there are many existing .NET implementations but they all share similar problems:\n\n- Verbose syntax for declaration and usage;\n- Many intrusive extensions methods that pollute project code;\n- Can only be used in certain contexts (delegates vs expressions);\n\n`Specification\u003cTSource\u003e` can solve all of these problems.\n\nYou can create specification using an expression:\n```csharp\n    Specification\u003cDepartmentEntity\u003e ActiveDepartment = new (\n        x =\u003e x.Active\n    );\n```\nor a delegate:\n```csharp\n    Specification\u003cDepartmentEntity\u003e ActiveDepartment = new (\n        default,\n        x =\u003e x.Active\n    );\n```\nor both (e.g. when you have to use EF specific DbFunctions):\n```csharp\n    Specification\u003cDepartmentEntity\u003e ActiveDepartment = new (\n        x =\u003e x.Active,\n        x =\u003e x.Active\n    );\n```\n\nYou can also easily combine specifications (using `\u0026\u0026`, `||`,`!` operators):\n```csharp\n    Specification\u003cDepartmentEntity\u003e CustomerServiceDepartment = new (\n        x =\u003e x.Name == \"Customer Service\"\n    );\n    \n    Specification\u003cDepartmentEntity\u003e ActiveCustomerServiceDepartment =  ActiveDepartment \u0026\u0026 CustomerServiceDepartment;\n```\n\nSpecifications can be nested:\n```csharp\n    Specification\u003cDepartmentEntity\u003e CustomerServiceDepartment = new (\n        x =\u003e x.Name == \"Customer Service\"\n    );\n    \n    Specification\u003cUserEntity\u003e ActiveUserInCustomerServiceDepartment = new (\n        x =\u003e x.Active \u0026\u0026 x.Departments.Any(CustomerServiceDepartment.IsSatisfiedBy)\n    );\n```\n\nFull example:\n\n```csharp\npublic class UserEntity\n{\n    public int Id { get; set; }\n\n    public string Name { get; set; }\n\n    public bool Active { get; set; }\n\n    public bool IsAdmin { get; set; }\n\n    public List\u003cDepartmentEntity\u003e Departments { get; set; }\n}\n\npublic class DepartmentEntity\n{\n    public int Id { get; set; }\n\n    public bool Active { get; set; }\n\n    public string Name { get; set; }\n}\n\npublic class UserDto\n{\n    public string Name { get; set; }\n\n    public List\u003cDepartmentDto\u003e Departments { get; set; }\n}\n\npublic class DepartmentDto\n{\n    public string Name { get; set; }\n}\n\npublic static class UserSpec\n{\n    public static readonly Specification\u003cDepartmentEntity\u003e ActiveDepartment = new(\n        x =\u003e x.Active\n    );\n\n    public static readonly Specification\u003cUserEntity\u003e ActiveUser = new(\n        x =\u003e x.Active\n    );\n\n    public static readonly Specification\u003cUserEntity\u003e AdminUser = new(\n        x =\u003e x.IsAdmin\n    );\n\n    public static readonly Specification\u003cUserEntity\u003e ActiveAdminUser = ActiveUser \u0026\u0026 AdminUser;\n\n    public static readonly Specification\u003cUserEntity\u003e ActiveUserInActiveDepartment = new(\n        x =\u003e x.Active \u0026\u0026 x.Departments.Any(ActiveDepartment)\n    );\n}\n\npublic class UserController : Controller\n{\n    private readonly DbContext _context;\n\n    public UserController(DbContext context)\n    {\n        _context = context;\n    }\n\n    public Task\u003cUserEntity\u003e GetUser(int id)\n    {\n        return context.Set\u003cUserEntity\u003e()\n            .Where(ActiveUserInActiveDepartment)\n            .Where(x =\u003e x.Id == id)\n            .SingleAsync();\n    }\n\n    public Task\u003cUserEntity\u003e GetAdminUser(int id)\n    {\n        return context.Set\u003cUserEntity\u003e()\n            .Where(ActiveAdminUser)\n            .Where(x =\u003e x.Id == id)\n            .SingleAsync();\n    }\n}\n```\n\nAdded `SpecificationVisitor` to rewrite specifications to plain expressions, can be used with EF.\n\n## Projections\n\nMy initial goal was to replace packages like AutoMapper and similar.\n\nThe common drawbacks of using mappers:\n\n- IDE can not show code usages, mappings are resolved in runtime (sometimes source generators are used);\n- API is complex yet limited in many cases;\n- Maintenance costs are high, authors frequently change APIs without considering other options;\n- Do not properly separate instance API (mapping object instances) and expression API (mapping through LINQ projections) which leads to bugs in runtime;\n- Despite all the claims you can not be sure in anything unless you manually test mapping of each field and each scenario (instance/LINQ);\n- Poor testing experience, sometimes you have to create your own \"tools\" specifically for testing mappings;\n- Compatibility with LINQ providers, recently AutoMapper has broken compatibility with EF6 for no reason at all;\n\nIn most cases mapping splits into two independent scenarios:\n\n1. Fetch DTOs from DB using automatic projections;\n2. Map DTOs to entities and then save modified entities to DB;\n\nIn reality direct mapping from DTO to entity is rarely viable: there are validations, access rights, business logic. It means that you end up writing custom code for each save operation.\n\nIn case we want to support only 1st scenario there is no need to deal with complex mapper configurations.\n\n`Projection\u003cTSource, TResult\u003e` - provides an option to define reusable mapping.\n\nYou can create projection using mapping expression:\n\n```csharp\n    Projection\u003cDepartmentEntity, DepartmentDto\u003e DepartmentDtoProjection = new (\n        x =\u003e new DepartmentDto\n        {\n            Name = x.Name\n        }\n    );\n```\nor delegate:\n\n```csharp\n    Projection\u003cDepartmentEntity, DepartmentDto\u003e DepartmentDtoProjection = new (\n        default,\n        x =\u003e new DepartmentDto\n        {\n            Name = x.Name\n        }\n    );\n```\n\nor both (e.g. when DB only features are used like DBFunctions, delegate should match DB behavior):\n\n```csharp\n    Projection\u003cDepartmentEntity, DepartmentDto\u003e DepartmentDtoProjection = new (\n        x =\u003e new DepartmentDto\n        {\n            Name = x.Name\n        },\n        x =\u003e new DepartmentDto\n        {\n            Name = x.Name\n        }\n    );\n```\n\nYou can use projections in other projections.\n\nThanks to `DelegateDecompiler` package and built-in ability to compile expression trees all of the options above will work but with different performance implications.\n\nFull example, controller should return only active users and users should have only active departments:\n\n```csharp\npublic class UserEntity\n{\n    public int Id { get; set; }\n\n    public string Name { get; set; }\n\n    public bool Active { get; set; }\n\n    public List\u003cDepartmentEntity\u003e Departments { get; set; }\n}\n\npublic class DepartmentEntity\n{\n    public int Id { get; set; }\n\n    public bool Active { get; set; }\n\n    public string Name { get; set; }\n}\n\npublic class UserDto\n{\n    public string Name { get; set; }\n\n    public List\u003cDepartmentDto\u003e Departments { get; set; }\n}\n\npublic class DepartmentDto\n{\n    public string Name { get; set; }\n}\n\npublic static class UserProjections\n{\n    public static readonly Projection\u003cDepartmentEntity, DepartmentDto\u003e DepartmentDtoProjection = new (\n        x =\u003e new DepartmentDto\n        {\n            Name = x.Name\n        }\n    );\n\n    public static readonly Projection\u003cUserEntity, UserDto\u003e UserDtoProjection = new (\n        x =\u003e new UserDto\n        {\n            Name = x.Name,\n            Departments = x.Departments\n                                .Where(z =\u003e z.Active)\n                                .Select(DepartmentDtoProjection.Project)\n                                .ToList()\n        }\n    );\n}\n\npublic class UserController : Controller \n{\n    private readonly DbContext _context;\n\n    public UserController(DbContext context)\n    {\n        _context = context;\n    }\n\n    // option 1: DB projection\n    public Task\u003cUserDto\u003e GetUser(int id)\n    {\n        return context.Set\u003cUserEntity\u003e()\n                .Where(x =\u003e x.Active)\n                .Where(x =\u003e x.Id == id)\n                .Select(UserProjections.UserProjection.ProjectExpression)\n                .SingleAsync();\n    }\n\n    // option 2: in-memory projection\n    public async Task\u003cUserDto\u003e GetUser(int id)\n    {\n        var user = await context.Set\u003cUserEntity\u003e()\n                     .Include(x =\u003e x.Departments)\n                     .Where(x =\u003e x.Active)\n                     .Where(x =\u003e x.Id == id)\n                     .SingleAsync();\n\n        return UserProjections.UserProjection.Project(user);\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkemsky%2Fprojection-tools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkemsky%2Fprojection-tools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkemsky%2Fprojection-tools/lists"}