-
-
Notifications
You must be signed in to change notification settings - Fork 158
Entity and Resource separation #344
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
Conversation
Add new MappingResourceService that can be used to separate and map between Entity Framework objects and JSON-API data transfer objects Added new example project, ResourceEntitySeparationExample, showing how to configure an entity/resource split and corresponding tests.
…ying the internal name instead of only inferring it from the dto variable name, as that may not match the entity relationship name
…-many entity required by ef core
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for getting this started! It's a much needed feature and I can tell you've put a lot of work into it and really appreciate the help here. I still need some more time to review everything, but I'm interested in your thoughts on the comments I've made so far. I've discussed the design proposed by #112 with a few others but could definitely use some extra feedback on it. Nothing is set in stone, so feel free to dispute any of the points I've made.
One quick question too. You said:
assumed convention of naming the mapped properties the same
You're referring to your specific AutoMapper implementation in the example app? If not I'm not sure that this should be a framework requirement. I think mapping from differently named resource/entity property names should definitely be allowed.
|
||
namespace JsonApiDotNetCoreExample.Migrations | ||
{ | ||
public partial class ResourceSeparation : Migration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
migrations shouldn't be necessary since we don't actually use them to build the db (we use the EnsureCreated
api). I thought I had removed all the migrations earlier, but apparently not, so I completely understand the confusion. I've added a note to #290 to improve our docs around this.
context.Database.EnsureCreated(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
@@ -21,7 +21,7 @@ public class Person : Identifiable, IHasMeta | |||
[HasMany("todo-collections")] | |||
public virtual List<TodoItemCollection> TodoItemCollections { get; set; } | |||
|
|||
[HasOne("unincludeable-item", Link.All, canInclude: false)] | |||
[HasOne("unincludeable-item", documentLinks: Link.All, canInclude: false)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -0,0 +1,234 @@ | |||
## Ignore Visual Studio temporary files, build results, and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shouldn't be necessary. it should be inherited from the root .gitignore
; | ||
|
||
CreateMap<CourseResource, CourseEntity>() | ||
// .ForMember(e => e.Registrations, opt => opt.MapFrom(r => RegistrationsFromResource(r))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
☝️ forgotten comment line ?
Number = e.Number, | ||
Title = e.Title, | ||
Description = e.Description | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the manual mapping? Seems like if we're already using AutoMapper, this shouldn't be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit here is for mapping the underlying many to many 3 entities to a more logical 2 resource setup. This really just provides the direct link from Student to Courses without having to go through the CourseStudent artificial resource.
: base(publicName, documentLinks, canInclude) | ||
{ | ||
_explicitIdentifiablePropertyName = withForiegnKey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙀
@@ -133,7 +116,7 @@ private bool ShouldIncludeRelationships() | |||
var relationship = _jsonApiContext.ContextGraph | |||
.GetContextEntity(typeof(T)) | |||
.Relationships | |||
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName); | |||
.FirstOrDefault(r => r.PublicRelationshipName == relationshipName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome catch! Should actually use: r.Is(relationshipName)
public virtual bool Is(string publicRelationshipName) | |
=> string.Equals(publicRelationshipName, PublicRelationshipName, StringComparison.OrdinalIgnoreCase); |
|
||
namespace JsonApiDotNetCore.Services | ||
{ | ||
public class MappingResourceService<TResource, TEntity> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to not duplicate the actual implementation details. My thought was that we would have a single, generalized implementation that would look like:
public ResourceService<TResource>
: ResourceService<TResource, int>, IResourceService<TResource>
{ /* ... */ }
public ResourceService<TResource, TId>
: ResourceService<TResource, TResource, TId>, IResourceService<TResource, TId>
{ /* ... */ }
public ResourceService<TResource, TEntity, TId>
: IResourceService<TResource, TEntity, TId>
{
public ResourceService(
IJsonApiContext jsonApiContext,
IEntityRepository<TEntity, TId> entityRepository,
ILoggerFactory loggerFactory)
{
// no mapper provided, TResource & TEntity must be the same type
if(typeof(TResource) != typeof(TEntity)) throw new InvalidOperationException("...");
}
public ResourceService(
IJsonApiContext jsonApiContext,
IEntityRepository<TEntity, TId> entityRepository,
ILoggerFactory loggerFactory,
IMapper mapper) { /* ... */ }
}
Now obviously this would allow for runtime errors, so #326 could include checking to verify at startup.
IJsonApiContext jsonApiContext, | ||
IEntityRepository<TEntity> entityRepository, | ||
ILoggerFactory loggerFactory, | ||
IMapper mapper) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As already stated, we don't want a hard dependency on AutoMapper. As outlined in #112, it should take an internally defined interface such as a IResourceMap<TResource, TEntity>
and not make any assumptions about a single general map instance. This also enables mapper agnostic startup validation as stated in the previous comment.
Example:
services.AddScoped<IResourceService<FooResource, FooEntity, int>>();
services.AddSingleton<IResourceMap<FooResource, FooEntity>>(
new AutoMapperAdapter<FooResource, FooEntity>()
);
// OR this could be bundled into a JsonApiDotNetCore.AutoMapper extension method
// on IServiceCollection like:
services.AddAutoMappedResource<FooResource, FooEntity>();
By doing this we can do a few things at startup:
- For every resource, check to see if an
IResourceService<TResource, TEntity, int>
implementation exists and has been registered on the container - If the implementation exists, check to see if an
IResourceMap<FooResource, FooEntity>
exists and has been registered on the container - Also, the
AutoMapperAdapter
(or whatever it is called) can verify the mapping is actually defined.
If these checks fail, we can provide quick feedback that a configuration error has occurred:
- A mapper has not been registered
(TResource != TEntity && MapperIsRegistered() == false)
- The AutoMapper.Mapper does not define a required mapping
- maybe others ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. Okay, yeah.i misunderstood that bit. Makes sense.
@@ -0,0 +1,234 @@ | |||
## Ignore Visual Studio temporary files, build results, and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't be necessary
/// for that relationship, due to the need to make the underlying | ||
/// framework and mapping understand the explicit navigation entity, | ||
/// a mirroring DTO resource is also required. | ||
/// </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 I think we can find a way to get around this...
Maybe not in v1, but it would be a pretty cool feature if we could allow applications to hide this implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. This bit was really only needed for patching the many to many relationship due to the framework not immediately knowing anything about the entity and the genericprocessor not knowing what to do with it.
I really wanted to avoid creating this but thought the rest might be valuable enough to postpone this bit.
Thanks for the feedback and catches. Next step is generalizing out the mapping resource. |
…it untyped for now to match how automapper works
the last question i have here is around the 112 conversation with configuration checking. for the moment, i've left IResourceMapper untyped really only because that's how IMapper works and i wanted to be able to inject an AutoMapper configuration pretty directly on to it like in
and Startup
What are your thoughts on that? Ideas on how to bridge that gap from AutoMapper's overall approach to type specific IResourceMapper implementations? |
🤔 so, from a discovery/validation perspective Also, if you need type specific overrides, you can just create a different implementation or interface and depend on that instead. So, let's say I use AutoMapper for 99% of my mapping needs, but there is this one resource/entity pair where I need something else. I would achieve that by doing something like: // CustomMapper.cs
public class FooMapper : IResourceMapper { /* ... */ }
// Startup.cs
services.AddSingleton(new FooMapper());
// FooService.cs
public class FooService : DefaultResourceService {
public FooService(
IJsonApiContext jsonApiContext,
IEntityRepository<Bar> entityRepository,
ILoggerFactory loggerFactory,
FooMapper mapper // <----- need to inject concrete type (or custom abstraction)
) : base(jsonApiContext, entityRepository, loggerFactory, mapper) { /* ... */ }
} So, I think that should be fine. I'll need to think about other scenarios as well. |
|
||
namespace JsonApiDotNetCore.Services | ||
{ | ||
public class DefaultResourceService<TResource> : DefaultResourceService<TResource, int>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to rename this class just yet. This would result in a wide-spread breaking change (i think we should be able to roll out the first version of this in a minor version). I'd prefer to hold off until #317. I can add this as a suggestion to the open issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good deal. Undoing the rename.
I've retargeted to the develop branch so I can publish a release to our MyGet feed (https://www.myget.org/F/research-institute/api/v3/index.json). I'll be integrating this into a large application tomorrow and if everything goes smoothly, I'll merge into master early this week. Thanks again for all your hard work! 🍻 |
Awesome! Thanks for all the feedback along the way! |
Closes #112
A couple of specific notes RE: the implementation.
Finally, an additional note. Initially I did not want to include resource representations of the many-to-many entities that entity framework requires, but the additional complexity for mapping these out to relationships the context graph would know about does not seem to provide real value, since you can accomplish the same via a resource declaration.