Skip to content

RFC: Resource to Entity Mapping #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jaredcnance opened this issue May 19, 2017 · 1 comment
Closed

RFC: Resource to Entity Mapping #112

jaredcnance opened this issue May 19, 2017 · 1 comment
Labels
enhancement help wanted RFC Request for comments. These issues have major impact on the direction of the library.
Milestone

Comments

@jaredcnance
Copy link
Contributor

jaredcnance commented May 19, 2017

Start Date: N/A
Status: WIP
RFC PR: N/A

Summary

Allow models to map to more than 1 resource.

Terminology

  • Entity refers to the database model. For EF users this is the POCO class that corresponds to a DbSet<> on the DbContext
  • Resource refers to the model exposed by the API

Motivation

Currently, there is a 1:1 relationship between the database entity and the exposed resource. There may be a need for different forms of an entity to be exposed. I see two different needs:

  • 1 entity : N resources (one-to-many)
  • N entities : 1 resource (many-to-one)

I believe the second need is already supported since this would require a custom implementation at the repository layer. The first however should be supported out-of-the-box and it currently is not.

Detailed Design

I think this is a relatively simple solution and that is to change the current dependency graph from:

|- Controller<TEntity>
|--- IResourceService<TEntity>
|------ IRepository<TEntity>

to

|- Controller<TResource>
|--- IResourceService<TResource, TEntity>
|------ IRepository<TEntity>

This shows that mapping would take place in the service layer. An application could define a mapping that, if exists, would be used to map the final entity result (post-database call) into the resource. For this, I think it is safe to choose AutoMapper as the standard mapping tool. Users would then define their maps using the ContextGraphBuilder:

options.BuildContextGraph((builder) => {
  builder.UseAutoMapper(AutoMapperConfig.GetMapper());
});

and the controller might look like:

public class UsersController : JsonApiController<User>
{
    public UsersController(
        IJsonApiContext jsonApiContext, 
        IResourceService<User, Person> resourceService, 
        ILoggerFactory loggerFactory) 
        : base(jsonApiContext, resourceService, loggerFactory)
    { }
}

ContextGraphBuilder

The ContextGraphBuilder will have a dictionary of mappings:

Dictionary<Type, List<IResourceMap>> _resourceMaps;

Calling builder.UseAutoMapper(AutoMapperConfig.GetMapper() will create the resource maps for every entity. All ContextEntities will be assigned an EmptyMapping which will handle calls for mapping TResource to TEntity when TResource == TEntity:

_resourceMaps[typeof(UnMappedEntity)] = new List<IResourceMap> { new EmptyMapping() };

As the ContextEntities are created, they will be assigned their maps:

_entities.Add(new ContextEntity
{
    Mappings = _resourceMaps[entityType],
    // ...
});

Resource Services

In order to maintain backwards compatibility, the current resource service definitions with generic parameters should remain available. We can define an additional generic parameter overload:

public interface IResourceService<TEntity> : IResourceService<TEntity, int>
{}

public interface IResourceService<TEntity, TId> : IResourceService<TEntity, TEntity, TId>
{}

public interface IResourceService<TResource, TEntity, TId>
{
 // ...
}

A IResourceService implementation should then perform the mapping prior to returning the resource:

public EntityResourceService(
    IJsonApiContext jsonApiContext,
    // ...
    )
{
    _jsonApiContext = jsonApiContext;
    _mapper = jsonApiContext.ContextGraph.GetMapper<TResource, TEntity>();
    // ...
}

public async Task<T> GetAsync(TId id)
{
    // ...
    var entity = //...
    var resource = _mapper.Map(entity); 
    return entity;
}

Automapper IResourceMap

public class AutoMapperService<TResource, TEntity>  
  : IResourceMap<TResource, TEntity>
{
  private readonly IMapper _mapper;
  public AutoMapperService(IMapper mapper) {
    _mapper = mapper;
  }

  public TResource Map(TEntity entity) 
     => _mapper.Map<TEntity, TResource>(entity);

  public IEnumerable<TResource> Map(IEnumerable<TEntity> entities) 
     => _mapper.Map<IEnumerable<TEntity>, IEnumerable<TResource>>(entities);

  public TEntity Map(TResource resource) 
     => _mapper.Map<TResource, TEntity>(resource);

  public string GetEntityRelationshipName(string resourceRelationshipName) {
     // ...
  }
}

Unspecified Mappings

If a user defines a controller but no mapping has been defined, a 500 error will be returned.

Relationships

Mappings will be used to translate relationship names. In the following example, a request for include=owner would map to Entity.User:

Mapper.Initialize(cfg => 
      cfg.CreateMap<Entity, Resource>()
    	.ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.User)));

// ...
public class Entity : Identifiable {
  public virtual Person User { get; set; }
}

public class Resource : Identifiable {
  [HasOne("owner")]
  public Person Owner { get; set; }
}

Validation of these definitions should occur on app startup.

Constraints

The types TResource and TEntity MUST implement IIdentifiable<TId> where TId is of the same type for both classes:

public interface IResourceService<TResource, TEntity, TId> 
  where TResource : class, IIdentifiable<TId>
  where TEntity : class, IIdentifiable<TId>
@jaredcnance jaredcnance added this to the v2.1.0 milestone May 27, 2017
@jaredcnance jaredcnance modified the milestones: v2.1.0, v2.2.0 Jun 12, 2017
@williamwong
Copy link

I like this idea a lot, and it's something that's missing in a lot of implementations of the JSONAPI spec. Most jump straight to the idea that there is a 1:1 relationship between a resource and a database table/entity. Having resource-specific controllers rather than entity-specific controllers is a step in the right direction. Frequently I find myself needed to create new endpoints (i.e., resources) that "flatten" out heavily-normalized datasets so that I don't have to do a lot of joining of the included entities on the client.

@jaredcnance jaredcnance modified the milestones: v2.2.0, v3.0.0 Dec 7, 2017
@jaredcnance jaredcnance changed the title [RFC] Resource to Entity Mapping RFC: Resource to Entity Mapping Apr 11, 2018
@jaredcnance jaredcnance added the RFC Request for comments. These issues have major impact on the direction of the library. label Jun 10, 2018
jaredcnance added a commit that referenced this issue Jul 23, 2018
feat(#112): Entity and Resource separation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement help wanted RFC Request for comments. These issues have major impact on the direction of the library.
Development

No branches or pull requests

2 participants