Code Monkey home page Code Monkey logo

projection-tools's Introduction

Build

Projection Tools

This package provides primitives for building reusable LINQ projections and specifications.

Package is available on Nuget.

Install using dotnet CLI:

dotnet add package ProjectionTools

Install using Package-Manager console:

PM> Install-Package ProjectionTools

I've also published an article on Medium Alternative specification pattern implementation in C#.

Specifications

Predicates can be complex, often 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 (delegates vs expressions);

Specification<TSource> can solve all of these problems.

You can create specification using an expression:

    Specification<DepartmentEntity> ActiveDepartment = new (
        x => x.Active
    );

or a delegate:

    Specification<DepartmentEntity> ActiveDepartment = new (
        default,
        x => x.Active
    );

or both (e.g. when you have to use EF specific DbFunctions):

    Specification<DepartmentEntity> ActiveDepartment = new (
        x => x.Active,
        x => x.Active
    );

You can also easily combine specifications (using &&, ||,! operators):

    Specification<DepartmentEntity> CustomerServiceDepartment = new (
        x => x.Name == "Customer Service"
    );
    
    Specification<DepartmentEntity> ActiveCustomerServiceDepartment =  ActiveDepartment && CustomerServiceDepartment;

Specifications can be nested:

    Specification<DepartmentEntity> CustomerServiceDepartment = new (
        x => x.Name == "Customer Service"
    );
    
    Specification<UserEntity> ActiveUserInCustomerServiceDepartment = new (
        x => x.Active && x.Departments.Any(CustomerServiceDepartment.IsSatisfiedBy)
    );

Full example:

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

    public string Name { get; set; }

    public bool Active { get; set; }

    public bool IsAdmin { 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 UserSpec
{
    public static readonly Specification<DepartmentEntity> ActiveDepartment = new(
        x => x.Active
    );

    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
{
    private readonly DbContext _context;

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

    public Task<UserEntity> GetUser(int id)
    {
        return context.Set<UserEntity>()
            .Where(ActiveUserInActiveDepartment)
            .Where(x => x.Id == id)
            .SingleAsync();
    }

    public Task<UserEntity> GetAdminUser(int id)
    {
        return context.Set<UserEntity>()
            .Where(ActiveAdminUser)
            .Where(x => x.Id == id)
            .SingleAsync();
    }
}

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 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;

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:

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

or delegate:

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

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

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

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:

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);
    }
}

projection-tools's People

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

projection-tools's Issues

Try source generators to avoid runtime compilation/decompilation

public static partial class Specifications
{
    private static readonly Specification<string> MySpec1Generate = new Specification<string>(x => x.Length > 0);

    private static readonly Specification<string> MySpec2Generate = new Specification<string>(x => x.Length > 5) && MySpec1Generate;

    private static readonly Specification<string> MySpec3Generate = new (x => MySpec2Generate.IsSatisfiedBy(x));
}

public static partial class SpecificationsGenerated
{
    public static readonly Specification<string> MySpec1 = new (x => x.Length > 0, x => x.Length > 0);

    public static readonly Specification<string> MySpec2 = new (x => x.Length > 5 && x.Length > 0, x => x.Length > 5 && x.Length > 0);

    public static readonly Specification<string> MySpec3 = new (x => x.Length > 5 && x.Length > 0, x => x.Length > 5 && x.Length > 0);
}

This can eliminate all expression machinery, delegate compilation and de-compilation.

How can we handle more complex cases when we reuse specification objects?

https://www.thinktecture.com/net/roslyn-source-generators-introduction/

Add optional flag to force eager evaluation

Some errors may be observed only in runtime, we can add a flag to trigger eager evaluation and get immediate feedback on start. For example, it can be enabled only in debug mode.

Optional/Conditional mapping

Sometimes we may want to control when fields should be mapped.

    Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
        (x, context) => new DepartmentDto
        {
            Name = x.Name,
             // marker compatible with delegates, can we keep original Description value?
            Description = context.When(context.WithDescription, x.Description /* default value? */)
        }
    );

dotnet/csharplang#5588

Figure out how make ContinuousIntegrationBuild work

image

dotnet validate package local ProjectionTools.1.0.11.nupkg 
Validating /home/dookie/Documents/projects/ProjectionTools/ProjectionTools.1.0.11.nupkg
• Source Link: ✅ Valid

• Deterministic (dll/exe): ❌ Non deterministic
    Ensure that the following property is enabled for CI builds
    and you're using at least the 2.1.300 SDK:
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
    The following assemblies have not been compiled with deterministic settings:
    lib\netstandard2.0\ProjectionTools.dll

• Compiler Flags: ✅ Valid

dotnet/sdk#16325

Using NuGetPackageExplorer:

dotnet validate package local ProjectionTools.1.0.XX.nupkg

Rewrite extension methods

Custom extension methods break EF translation, but we can rewrite them.

public static readonly Projection<DepartmentEntity, DepartmentDto> DepartmentDtoProjection = new (
    x => new DepartmentDto
    {
        Name = x.Name,
        Status = x.Status.ToDtoStatus()
    }
);
public static StatusDto ToDtoStatus(this Status status)
{
    return StatusDtoProjection.Project(status);
}

Can be translated to:

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.