json-api-dotnet/JsonApiDotNetCore

Include relations inside custom controller

Andreamarcelli opened this issue · 7 comments

SUMMARY

Hi to all, i try to return an entity with its relations, but when i return the result the relations is not included.

DETAILS

i wrote a controller like this

public class UsersController : JsonApiController<User, String>
{
private IJsonApiManualService _jsonApiManualService;
protected readonly UserManager _userManager;
protected readonly IDocumentService _docService;
private readonly MyDbContext _context;

[ActivatorUtilitiesConstructor]
public UsersController(IJsonApiManualService jsonApiManualService
    , IJsonApiOptions options, IResourceGraph resourceGraph
    , ILoggerFactory loggerFactory
    , IResourceService<User, String> resourceService
    , MyDbContext context) : base(options, resourceGraph, loggerFactory, resourceService)
{
    _jsonApiManualService = jsonApiManualService;
    _context = context;
}

.
.
.

and defined a query like this

var query = _context.Users.Include(u => u.UserTenants).Where(u => u.UserTenants.Any(ut => ut.Tenant.Id == 999));

var users = query.ToList();
return Ok(users);

inside users i see the result and for each one i see the relation that i've included with the value.

but inside postman the include is not present

Thanks to all.

VERSIONS USED

  • JsonApiDotNetCore version: 5.1.2
  • ASP.NET Core version: 6
  • Entity Framework Core version: 6
  • Database provider: sql server

Hi @Andreamarcelli, thanks for asking.

The way JADNC works is that the response serializer needs to know which relationships need to be rendered. When using the built-in pipeline (controller/service/repository), this information is automatically derived from the include query string parameter. To do this manually, inject IEvaluatedIncludeCache and call its Set method. To construct its IncludeExpression parameter, you can inject IIncludeParser and call its Parse method that takes a string.

However, if your goal is to support multi-tenancy, we have existing samples for that. See https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/DatabasePerTenantExample and https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy.

Thanks for the answer.
I've a problem and i not understand how to fix, i paste my code:

ignore the override of the delete and the filter on tenant 999, is only for demo

`{
public class UsersController : JsonApiController<User, String>
{
private IJsonApiManualService _jsonApiManualService;
protected readonly UserManager _userManager;
protected readonly IDocumentService _docService;
private readonly MyDbContext _context;
private IEvaluatedIncludeCache _evaluatedIncludeCache;
private IncludeParser _includeParser;

    [ActivatorUtilitiesConstructor]
    public UsersController(IJsonApiManualService jsonApiManualService
        , IJsonApiOptions options, IResourceGraph resourceGraph
        , ILoggerFactory loggerFactory
        , IResourceService<User, String> resourceService
        , MyDbContext context
        , IEvaluatedIncludeCache evaluatedIncludeCache
        , IncludeParser includeParser) : base(options, resourceGraph, loggerFactory, resourceService)
    {
        _jsonApiManualService = jsonApiManualService;
        _context = context;
        _evaluatedIncludeCache = evaluatedIncludeCache;
        _includeParser = includeParser;
    }


    [HttpDelete("{id}")]
    public override async Task<IActionResult> DeleteAsync(String id, CancellationToken cancellationToken)
    {
        try
        {
            User ut = await _jsonApiManualService.DeleteUser(id);

            if (ut == null)
            {
                return StatusCode(404);
            }

            return StatusCode(204);
        }
        catch (Exception ex)
        {
            return StatusCode(500, ex.Message);
        }
    }


 

    [HttpGet]
    [Route("statistics")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status405MethodNotAllowed)]
    [ProducesResponseType(StatusCodes.Status406NotAcceptable)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
         public async Task<IActionResult> Statistics()
    {
        _evaluatedIncludeCache.Set(_includeParser.Parse("UserTenants", new ResourceType("User",typeof(User), typeof(String)), 10));
        var query = _context.Users.Include(u => u.UserTenants).Where(u => u.UserTenants.Any(ut => ut.Tenant.Id == 999));
        dynamic users = query.ToList();
        return Ok(users);
    }
}

}`

the error is:
"Relationship 'UserTenants' does not exist on resource type 'User'."

but inside User i have
[HasMany]
public ICollection UserTenants { get; set; }

In that case, inject IResourceGraph and obtain the resource type from it, and then its relationship.

I don't think ICollection would work, it should be ICollection<UserTenant>.
And remove ProducesResponseType from your controller, OpenAPI won't work.

Any reason why you can't use one of the fully working samples? It'd save you quite some trouble.

Thanks for the answer, now works !
how you mean with the "one of the fully working samples" ?
i not found a sample of a custom controller that allow me to write some custom code and return a response in jsonapi format with relationship and other.
i know that i can use the autogenerated crud and use the include in the api call, but for some reason i need to do it in custom way to implement some business logic inside.

It demonstrates how to set up multi-tenancy using an EF Core HasQueryFilter in the DbContext, so tenant filtering works across all queries at all endpoints. See also https://learn.microsoft.com/en-us/ef/core/miscellaneous/multitenancy. If your entire API is tenant-aware, this is the proper way to set it up. If your API isn't, except for this single custom action method, then your approach should be fine.