Skip to content

Commit

Permalink
update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Turtsevich committed Sep 19, 2023
1 parent e342518 commit 04e8e59
Showing 1 changed file with 115 additions and 118 deletions.
233 changes: 115 additions & 118 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,73 +17,60 @@ PM> Install-Package ProjectionTools

I've also published an article on Medium [Alternative specification pattern implementation in C#](https://medium.com/@nimrod97/alternative-specification-pattern-implementation-in-c-f5d88a7ed364).

## Projections

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

The common drawbacks of using mappers:

- IDE can not show code usages, mappings are resolved in runtime (sometimes source generators are used);
- API is complex yet limited in many cases;
- Maintenance costs are high, authors frequently 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;
- 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, sometimes you have to create your own "tools" specifically for testing mappings;
- Compatibility with LINQ providers, recently AutoMapper has broken compatibility with EF6 for no reason at all;

In the most cases mapping splits into two independent scenarios:

1. Fetch DTOs from DB using automatic projections;
2. Map DTOs to entities and then save modified entities to DB;
## Specifications

In 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.
Predicates can be complex, often a combination of different predicates depending on business logic.

In case we want to support only 1st scenario there is no need to deal with complex mapper configurations.
There is a well-known specification pattern and there are many existing .NET implementations but they all share similar problems:

`Projection<TSource, TResult>` - provides an option to define reusable mappings.
- Verbose syntax for declaration and usage;
- Many intrusive extensions methods that pollute project code;
- Can only be used in certain contexts;

You can create projection using mapping expression:
`Specification<TSource>` can solve all of these problems.

You can create specification using an expression:
```csharp
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
x => new DepartmentDto
{
Name = x.Name
}
Specification<DepartmentEntity> ActiveDepartment = new (
x => x.Active
);
```
or delegate:

or a delegate:
```csharp
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
Specification<DepartmentEntity> ActiveDepartment = new (
default,
x => new DepartmentDto
{
Name = x.Name
}
x => x.Active
);
```

or both (e.g. when DB only features are used like DBFunctions, delegate should match DB behavior):

or both (e.g. when you have to use EF specific DbFunctions):
```csharp
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
x => new DepartmentDto
{
Name = x.Name
},
x => new DepartmentDto
{
Name = x.Name
}
Specification<DepartmentEntity> ActiveDepartment = new (
x => x.Active,
x => x.Active
);
```

You can use projections in other projections.
You can also easily combine specifications (using `&&`, `||`,`!` operators):
```csharp
Specification<DepartmentEntity> CustomerServiceDepartment = new (
x => x.Name == "Customer Service"
);

Specification<DepartmentEntity> ActiveCustomerServiceDepartment = ActiveDepartment && CustomerServiceDepartment;
```

Thanks to `DelegateDecompiler` package and built-in ability to compile expression trees all of the options above will work but with different performance implications.
Specifications can be nested:
```csharp
Specification<DepartmentEntity> CustomerServiceDepartment = new (
x => x.Name == "Customer Service"
);

Specification<UserEntity> ActiveUserInCustomerServiceDepartment = new (
x => x.Active && x.Departments.Any(CustomerServiceDepartment.IsSatisfiedBy)
);
```

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

```csharp
public class UserEntity
Expand All @@ -94,6 +81,8 @@ public class UserEntity

public bool Active { get; set; }

public bool IsAdmin { get; set; }

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

Expand All @@ -118,28 +107,28 @@ public class DepartmentDto
public string Name { get; set; }
}

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

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 static readonly Specification<UserEntity> ActiveUser = new(
x => x.Active
);

public static readonly Specification<UserEntity> AdminUser = new(
x => x.IsAdmin
);

public static readonly Specification<UserEntity> ActiveAdminUser = ActiveUser && AdminUser;

public static readonly Specification<UserEntity> ActiveUserInActiveDepartment = new(
x => x.Active && x.Departments.Any(ActiveDepartment)
);
}

public class UserController : Controller
public class UserController : Controller
{
private readonly DbContext _context;

Expand All @@ -148,75 +137,91 @@ public class UserController : Controller
_context = context;
}

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

// option 2: in-memory projection
public async Task<UserDto> GetUser(int id)
public Task<UserEntity> GetAdminUser(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);
return context.Set<UserEntity>()
.Where(ActiveAdminUser)
.Where(x => x.Id == id)
.SingleAsync();
}
}
```

## Specifications
## Projections

Projections work 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`.
My initial goal was to replace packages like AutoMapper and similar.

This predicates can be more complex, often a combination of different predicates depending on business logic.
The common drawbacks of using mappers:

There is a well-known specification pattern and there are many existing .NET implementations but they all share similar problems:
- IDE can not show code usages, mappings are resolved in runtime (sometimes source generators are used);
- API is complex yet limited in many cases;
- Maintenance costs are high, authors frequently 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;
- 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, sometimes you have to create your own "tools" specifically for testing mappings;
- Compatibility with LINQ providers, recently AutoMapper has broken compatibility with EF6 for no reason at all;

- Verbose syntax for declaration and usage;
- Many intrusive extensions methods that pollute project code;
- Can only be used in certain contexts;
In most cases mapping splits into two independent scenarios:

`Specification<TSource>` can solve these problems.
1. Fetch DTOs from DB using automatic projections;
2. Map DTOs to entities and then save modified entities to DB;

In 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.

In case we want to support only 1st scenario there is no need to deal with complex mapper configurations.

`Projection<TSource, TResult>` - provides an option to define reusable mapping.

You can create projection using mapping expression:

You can create specification using expression:
```csharp
Specification<DepartmentEntity> ActiveDepartment = new (
x => x.Active
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
x => new DepartmentDto
{
Name = x.Name
}
);
```
or delegate:

```csharp
Specification<DepartmentEntity> ActiveDepartment = new (
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
default,
x => x.Active
);
```
or both (e.g. when DB has case-insensitive collation, delegate should match DB behavior):
```csharp
Specification<DepartmentEntity> ActiveDepartment = new (
x => x.Active,
x => x.Active
x => new DepartmentDto
{
Name = x.Name
}
);
```

You can also combine specifications (using `&&`, `||`,`!`):
or both (e.g. when DB only features are used like DBFunctions, delegate should match DB behavior):

```csharp
Specification<DepartmentEntity> CustomerServiceDepartment = new (
x => x.Name == "Customer Service"
Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
x => new DepartmentDto
{
Name = x.Name
},
x => new DepartmentDto
{
Name = x.Name
}
);

Specification<DepartmentEntity> ActiveCustomerServiceDepartment = ActiveDepartment && CustomerServiceDepartment;
```

Full example:
You can use projections in other projections.

Thanks to `DelegateDecompiler` package and built-in ability to compile expression trees all of the options above will work but with different performance implications.

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

```csharp
public class UserEntity
Expand Down Expand Up @@ -253,14 +258,6 @@ public class DepartmentDto

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
{
Expand All @@ -273,7 +270,7 @@ public static class UserProjections
{
Name = x.Name,
Departments = x.Departments
.Where(ActiveDepartment)
.Where(z => z.Active)
.Select(DepartmentDtoProjection.Project)
.ToList()
}
Expand All @@ -289,11 +286,11 @@ public class UserController : Controller
_context = context;
}

// option 1: Db projection
// option 1: DB projection
public Task<UserDto> GetUser(int id)
{
return context.Set<UserEntity>()
.Where(ActiveUser)
.Where(x => x.Active)
.Where(x => x.Id == id)
.Select(UserProjections.UserProjection.ProjectExpression)
.SingleAsync();
Expand All @@ -304,7 +301,7 @@ public class UserController : Controller
{
var user = await context.Set<UserEntity>()
.Include(x => x.Departments)
.Where(ActiveUser)
.Where(x => x.Active)
.Where(x => x.Id == id)
.SingleAsync();

Expand Down

0 comments on commit 04e8e59

Please sign in to comment.