ProjectionTools 1.0.7
See the version list below for details.
dotnet add package ProjectionTools --version 1.0.7
NuGet\Install-Package ProjectionTools -Version 1.0.7
<PackageReference Include="ProjectionTools" Version="1.0.7" />
paket add ProjectionTools --version 1.0.7
#r "nuget: ProjectionTools, 1.0.7"
// Install ProjectionTools as a Cake Addin
#addin nuget:?package=ProjectionTools&version=1.0.7
// Install ProjectionTools as a Cake Tool
#tool nuget:?package=ProjectionTools&version=1.0.7
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 | Versions 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. |
-
.NETStandard 2.0
- DelegateDecompiler (>= 0.32.0)
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 | 96 | 4/11/2024 |
1.0.15 | 97 | 3/19/2024 |
1.0.14 | 130 | 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 |