ProjectionTools 1.0.8

There is a newer version of this package available.
See the version list below for details.
dotnet add package ProjectionTools --version 1.0.8
NuGet\Install-Package ProjectionTools -Version 1.0.8
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="ProjectionTools" Version="1.0.8" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ProjectionTools --version 1.0.8
#r "nuget: ProjectionTools, 1.0.8"
#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.
// Install ProjectionTools as a Cake Addin
#addin nuget:?package=ProjectionTools&version=1.0.8

// Install ProjectionTools as a Cake Tool
#tool nuget:?package=ProjectionTools&version=1.0.8

Build

Projection Tools

This package provides two primitives Projection<TSource, TResult> and Specification<TSource> for building reusable LINQ projections and predicates.

Projections

My initial goal was to replace packages like AutoMapper and similar.

The common drawbacks of using mappers:

  • Code "black hole" and dirty magic: IDE can not show code usages, mappings are resolved in runtime;
  • Complex API: API is complex yet limited in many cases;
  • Maintenance costs: authors often change APIs without considering other options;
  • Do not properly separate instance API (mapping object instances) and expression API (mapping through LINQ projections) which leads to bugs in runtime;
  • Bugs: despite all the claims you can not be sure in anything unless you manually test mapping of each field and each scenario (instance/LINQ);
  • Poor testing experience;

In the most cases mapping splits into two independent stages:

  • Fetch DTOs directly from DB using automatic projections and pass result to client;
  • Map incoming DTOs to entities to apply changes from client and then save modified entities to DB;

In reality mapping from DTO to entity is rarely a good idea: there are validations, access rights, business logic. It means that you end up using custom code in each case.

Projection<TSource, TResult> - provides option to define mapping from entity to DTO.

Quick example, controller should return only active users and users should have only active departments:

public class UserEntity
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool Active { get; set; }

    public List<DepartmentEntity> Departments { get; set; }
}

public class DepartmentEntity
{
    public int Id { get; set; }

    public bool Active { get; set; }

    public string Name { get; set; }
}

public class UserDto
{
    public string Name { get; set; }

    public List<DepartmentDto> Departments { get; set; }
}

public class DepartmentDto
{
    public string Name { get; set; }
}

public static class UserProjections
{
    public static readonly Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
        x => new DepartmentDto
        {
            Name = x.Name
        }
    );

    public static readonly Projection<UserEntity, UserDto> UserDtoProjection = new (
        x => new UserDto
        {
            Name = x.Name,
            Departments = x.Departments
                                .Where(z => z.Active)
                                .Select(DepartmentDtoProjection.Project)
                                .ToList()
        }
    );
}

public class UserController : Controller 
{
    private readonly DbContext _context;

    public UserController(DbContext context)
    {
        _context = context;
    }

    // option 1: DB projection
    public Task<UserDto> GetUser(int id)
    {
        return context.Set<UserEntity>()
                .Where(x => x.Active)
                .Where(x => x.Id == id)
                .Select(UserProjections.UserProjection.ProjectExpression)
                .SingleAsync();
    }

    // option 2: in-memory projection
    public async Task<UserDto> GetUser(int id)
    {
        var user = await context.Set<UserEntity>()
                     .Include(x => x.Departments)
                     .Where(x => x.Active)
                     .Where(x => x.Id == id)
                     .SingleAsync();

        return UserProjections.UserProjection.Project(user);
    }
}

Specifications (reusable predicates)

Projection works but we have a problem: we do not reuse Where(x => x.Active) checks. There is one predicate in UserController.GetUser method and another in UserDtoProjection.

This predicate can be more complex, often it is a combination of different predicates depending on business logic.

There is a well-known specification pattern and there are many existing .NET implementations but they all share similar problems:

  • Verbose syntax for declaration and usage;
  • Many intrusive extensions methods that pollute project code;
  • Can only be used in certain contexts;

This is how we can use Specification<TSource> to solve these problems:

public class UserEntity
{
    public int Id { get; set; }

    public string Name { get; set; }

    public bool Active { get; set; }

    public List<DepartmentEntity> Departments { get; set; }
}

public class DepartmentEntity
{
    public int Id { get; set; }

    public bool Active { get; set; }

    public string Name { get; set; }
}

public class UserDto
{
    public string Name { get; set; }

    public List<DepartmentDto> Departments { get; set; }
}

public class DepartmentDto
{
    public string Name { get; set; }
}

public static class UserProjections
{
    public static readonly Specification<DepartmentEntity> ActiveDepartment = new (
        x => x.Active
    );
    
    public static readonly Specification<UserEntity> ActiveUser = new (
        x => x.Active
    );

    public static readonly Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
        x => new DepartmentDto
        {
            Name = x.Name
        }
    );

    public static readonly Projection<UserEntity, UserDto> UserDtoProjection = new (
        x => new UserDto
        {
            Name = x.Name,
            Departments = x.Departments
                                .Where(ActiveDepartment)
                                .Select(DepartmentDtoProjection.Project)
                                .ToList()
        }
    );
}

public class UserController : Controller 
{
    private readonly DbContext _context;

    public UserController(DbContext context)
    {
        _context = context;
    }

    // option 1: Db projection
    public Task<UserDto> GetUser(int id)
    {
        return context.Set<UserEntity>()
                .Where(ActiveUser)
                .Where(x => x.Id == id)
                .Select(UserProjections.UserProjection.ProjectExpression)
                .SingleAsync();
    }

    // option 2: in-memory projection
    public async Task<UserDto> GetUser(int id)
    {
        var user = await context.Set<UserEntity>()
                     .Include(x => x.Departments)
                     .Where(ActiveUser)
                     .Where(x => x.Id == id)
                     .SingleAsync();

        return UserProjections.UserProjection.Project(user);
    }
}
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework 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
1.0.17 89 4/11/2024
1.0.15 96 3/19/2024
1.0.14 129 2/15/2024
1.0.13 401 7/3/2023
1.0.12 123 7/3/2023
1.0.11 127 7/3/2023
1.0.10 127 7/3/2023
1.0.9 126 7/3/2023
1.0.8 129 7/3/2023
1.0.7 126 7/3/2023
1.0.6 120 7/3/2023
1.0.5 125 7/3/2023
1.0.4 125 7/3/2023
1.0.3 123 7/3/2023
1.0.0 124 7/3/2023