json-api-dotnet / JsonApiDotNetCore

A framework for building JSON:API compliant REST APIs using ASP.NET and Entity Framework Core.
https://www.jsonapi.net
MIT License
662 stars 160 forks source link

Attribute unavailable & available if request #1539

Closed patcharees closed 2 months ago

patcharees commented 2 months ago

SUMMARY

If it is possible to make some attributes unavailable by default and vice versa if specify explicitly in the request?

DETAILS

I am working with a data collection in mongodb that is complex and contains many nested data structure (inside its own collection). I wonder if it is possible to define some [attr ?] attribute that make view of nested data structure unavailable by default, and vice versa if specify explicitly in the request somehow?

STEPS TO REPRODUCE

  1. Here is my resource class. Now all Refs1, Refs2, Refs3 publicly available. How can I define here to give me the behaviours I described? Is it possible?
public class Resource1 : Identifiable<string>
{

[Attr]
public string? id { get; set; }

[Attr]
public string? name { get; set; }

[Attr]
public List<Ref1>? Refs1 { get; set; }

[Attr]
public List<Ref2>? Refs2 { get; set; }

[Attr]
public List<Ref3>? Refs3 { get; set; }

}

VERSIONS USED

bkoelman commented 2 months ago

This can be done using a resource definition. See https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/pull/54 for an example. The second commit backports to the version you're using.

patcharees commented 2 months ago

I’m newbie. I have a lot of fields to exclude/include. Is there a better way to implement this than looping one by one?

public override SparseFieldSetExpression? OnApplySparseFieldSet(SparseFieldSetExpression? existingSparseFieldSet)
    {
        if (!_queryStringAccessor.Query.ContainsKey("fields[books]"))
        {
            return existingSparseFieldSet
                .Excluding<Book>(book => book.Refs1, ResourceGraph)
                .Excluding<Book>(book => book.Refs2, ResourceGraph)
                .Excluding<Book>(book => book.Refs3, ResourceGraph);
        }

        return existingSparseFieldSet;
    }
bkoelman commented 2 months ago

There's no looping in the code. You can ensure fields are always returned using the .Including method, which works similarly.

More importantly, JSON:API is loved by clients because it allows them to specify exactly what data to fetch and return. A server that is sending fields the client never asked for negates that optimized network bandwidth experience (between client and server, as well as between server and database). A better approach would be to break down your data structure into smaller parts, so you won't need to include/exclude so many fields.

patcharees commented 2 months ago

I got it. Thanks :)

patcharees commented 2 months ago

I created the following method to 1) return all DefaultAttributes (primitive & string) 2) return all DefaultAttributes + specific fields by fields[]

The first case works well. But the 2nd returns only the specific fields, even though I add more attributes into existingSparseFieldSet. Do you know why?

public override SparseFieldSetExpression? OnApplySparseFieldSet(SparseFieldSetExpression? existingSparseFieldSet)
{
    //var resourceContext = ResourceGraph.GetResourceContext<TResource>();
    // set default attribute first
    var attributes = _resourceGraph.GetResourceContext<TResource>().Attributes;
    var fields = "fields[" + _resourceGraph.GetResourceContext<TResource>().PublicName + "]";

    var defaultAttributes = (IReadOnlyCollection<ResourceFieldAttribute>)
        attributes.Where(a => PageConfigurableDefinition<TResource>.IsDefaultAttribute(a)).ToList();

    if (_queryStringAccessor.Query.ContainsKey(fields) && existingSparseFieldSet != null) 
    {
        HashSet<ResourceFieldAttribute> fieldSet = existingSparseFieldSet.Fields.ToHashSet();
        foreach (var attr in defaultAttributes)
            fieldSet.Add(attr);
        return new SparseFieldSetExpression(fieldSet);
    }
    else
    {
        return new SparseFieldSetExpression(defaultAttributes);
    }
}
patcharees commented 2 months ago

This does not work

    public override SparseFieldSetExpression? OnApplySparseFieldSet(SparseFieldSetExpression? existingSparseFieldSet)
    {
        return existingSparseFieldSet
                .Including<Book>(t => t.bookId, ResourceGraph)
                .Including<Book>(t => t.bookGroup, ResourceGraph)
                .Including<Book>(t => t.bookGroupName, ResourceGraph);
    }
bkoelman commented 2 months ago

You're right, using .Including() doesn't add extra fields to the JSON response. I assumed it would, but that's not the case. It is actually by design and documented at https://www.jsonapi.net/api/JsonApiDotNetCore.Resources.IResourceDefinition-2.html#JsonApiDotNetCore_Resources_IResourceDefinition_2_OnApplySparseFieldSet_JsonApiDotNetCore_Queries_Expressions_SparseFieldSetExpression__remarks:

Including extra fields from this method will retrieve them, but not include them in the json output. This enables you to expose calculated properties whose value depends on a field that is not in the sparse fieldset.

So in that case, what you want is not possible. You'll just need to send the desired fields in the query string.