Skip to content

Feat/#226: Add DocumentBuilderOptions allowing omission of null attributes #227

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

Merged
merged 2 commits into from
Jan 30, 2018

Conversation

lheiskan
Copy link
Contributor

Closes #226

FEATURE

  • write tests that address the requirements outlined in the issue
  • fulfill the feature requirements
  • bump package version

Looked into adding support for omitting null valued attributes from responses. This can be enabled globally by setting option 'OmitNullValuedAttributesFromResponses' to true. Default value for the option is false.

To consider before the merge: JsonApi is spec does not specify anything about omitting null values in responses - least as far as I can tell. It may be better to allow clients to opt-in for this functionality instead of having a global configuration, so only compliant clients can opt-in. What do you think?

@jaredcnance
Copy link
Contributor

thanks for this!

better to allow clients to opt-in for this functionality instead of having a global configuration

Not sure I understand what you mean here. The global configuration allows server applications to opt-in. How would you imagine a client application would opt-in?

return
(_requestMeta?.GetMeta()?.TryGetValue("jsonapidotnet:omit-null-valued-attributes", out isOmitNullValuedAttributes) ?? false ) ?
(bool)isOmitNullValuedAttributes
: _jsonApiContext.Options.OmitNullValuedAttributesFromResponses;
Copy link
Contributor

@jaredcnance jaredcnance Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows a client application to use this feature without explicit permission from the server. I would almost prefer a behavior enum in the options rather than a bool:

enum NullAttributeResponseBehavior {
  IncludeNullAttributes = 0, // default
  ExcludeNullAttributes = 1,
  OptionalForClients = 2
}

or something along those lines. thoughts?

@lheiskan
Copy link
Contributor Author

Sounds reasonable, however, would a separate setting that enabled ability for the clients to override the global default be more flexible? It would better cater for schenarios like

global: include nulls, client:exclude
global: exclude nulls, client:include

The latter would be useful for api discovery, if by default server decided to omit nulls. Using the override at development time, clients could easily find out what is available.

@jaredcnance
Copy link
Contributor

jaredcnance commented Jan 26, 2018

That's fine as long as the server application has 100% control of possible behavior. So, the API you're proposing might look like:

struct NullAttributeResponseBehavior {
   public NullAttributeResponseBehavior(bool includeNullAttributes = true, bool allowClientOverride = false) {
    //...
  }
  // ...
}
class JsonApiOptions {
  public NullAttributeResponseBehavior  NullAttributeResponseBehavior { get; set; } 
    = new NullAttributeResponseBehavior()
}

I think I would still prefer to have it grouped into a class separate from the JsonApiOptions class.

@lheiskan
Copy link
Contributor Author

lheiskan commented Jan 26, 2018

Cool - the above api sounds good.

One question: i'd like to implement IRequestMeta service that would provide access to the meta data in the incoming jsonapi request, just wondering if there is a way to hook this in somehow? Something like this might make a reasonable default implementation as well? It could be then used as a way for the client to specify the null value handling. Does this make sense?

{
"meta": {
"jsonapidotnet:omit-null-valued-attributes": false,
},
"data": {
// ...
}
}

@jaredcnance
Copy link
Contributor

jaredcnance commented Jan 26, 2018

IRequestMeta is actually for generating response meta. See the documentation and the usage. The misnomer was unintentional and was only meant to distinguish itself from IMeta which was specific to resource models. In retrospect I wish it had been named IResponseMetaService or something...

Currently, we don't do anything with the meta object during deserialization. It seems like adding something to the JsonApiContext might make sense since it's whole purpose is to carry ambient request information. The alternative would be to have an empty instance created in the scope whose properties are set in the deserializer. That might look like:

public class RequestMetaDocument : IDictionary<string, object>
{
   public bool OmitNullValuedAttributes => this.TryGetValue("omit-null-valued-attributes", out var result) ? result : false;
  // ...
}

and then the Document.Meta type could be updated to RequestMetaDocument.

public Dictionary<string, object> Meta { get; set; }

@lheiskan
Copy link
Contributor Author

lheiskan commented Jan 26, 2018

Ok, given the above - gave this another go with a different approach.

IDocumentBuilderBehavior api can be implemented by the developer and injected, it contains DocumentBuilderOptions that specify how to handle null valued attributes.

Also, added the meta element contents from request to IJsonApiContext.

if (ShouldIncludeAttribute(attr))
{
var value = attr.GetValue(entity);
if (value != null || _documentBuilderOptions.IncludeNullAttributes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 things:

  1. This probably belongs in the ShouldIncludeAttribute method, possibly as a call to another private method AttrIsNotNullAndIsNotExcluded() (or some better name).
  2. This doesn't take into consider the DocumentMeta field, so I'm guessing this is still WIP?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned this up a bit. My plan was to use the DocumentMeta in my own implementation if IDocumentBuilderBehavior - and that the interface could be similar to IRequestMeta in that library users can provide their own implementations. However, it might make sense to include a built in default implementation with the library - which I could do if you like.

{
public struct DocumentBuilderOptions
{
public DocumentBuilderOptions(bool isIncludeNullAttributes = false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's rename the parameter to includeNullAttributes

where were you planning on adding the setting to allow client overrides? maybe I'm missing something.

Copy link
Contributor Author

@lheiskan lheiskan Jan 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - IDocumentBuilderBehaviour allows injecting an implementation that produces document builder options, i.e. an implementation could allow for determining null handling based no the global options and information provided in document meta. So if we want to provide a built in behaviour with the features discussed, i thought the client overrides would be handled in that implementation.

@lheiskan
Copy link
Contributor Author

I can solve my current use case with this implementation: I can inject null handling options of my choosing, and access to document meta that allows using client preferences as part of that process. Sweet!

Should I include an implementation of IDocumentBuilderBehaviour to be included with the library? Could developers using the library override the behavior with their own? I'm happy to include one if it seems useful. I guess we pretty much know the features that the implementation would have from the above discussion.

@lheiskan
Copy link
Contributor Author

Hey - added an implementation for a document builder behavior.

  • If nothing is configured, nulls are not omitted and clients cannot affect this
  • Options has a 'NullAttributeResponseBehavior'. This can be used to configure the global default and to allow clients to request a specific type of handling
  • Clients can set omitNullValuedAttributes={true|false} query string parameter to request specific null handling. Server will ignore this if client overrides are not allowed.
  • Query parameter only works if the 'AllowCustomQueryParameters' option is on (it is not on by default)

I actually ended up not using the document.meta, as this is not available for all methods - so perhaps there is no need to expose that, or the exposing might be more suited to another pull request.

If clients want to implement some other kind of mechanism for null behaviour, this can be done by service.Replace the default IDocumentBuilderBehaviour with a custom implementation - allowing implementations that use http headers, claims in access tokens or any other implementation specific mechanism.

The spec has a section that defines how to name implementation specific query parameters: http://jsonapi.org/format/#query-parameters. I think camelCase used above is compatible but other formats are also possible. Do you have a preference?

Copy link
Contributor

@jaredcnance jaredcnance left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your hard work on this! If you have time, I'd also like to see an end-to-end test. After these changes I'm good with this PR. Which version are you running (2.1.x or 2.2-beta)? I'd like to only do a release to the 2.2-beta, which should be finalized next weekend. But, if you're on 2.1 I can back port it for you. I wouldn't want you to do all this work and not be able to use it immediately 😄

{
_jsonApiContext = jsonApiContext;
_contextGraph = jsonApiContext.ContextGraph;
_requestMeta = requestMeta;
_documentBuilderOptions = documentBuilderBehavior?.GetDocumentBuilderOptions() ?? _documentBuilderOptions;
Copy link
Contributor

@jaredcnance jaredcnance Jan 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend moving the new DocumentBuilderOptions() call here rather than eagerly allocating in the field initializer which may not be necessary if an instance of IDocumentBuilderBehavior is being provided by the DI framework. This also reduces misdirection making the code easier to read (i.e. as currently written I see that we're assigning a value to _documentBuilderOptions but I have to look up to line 16 to see the coalesced value). Super minor and not a blocker for me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, it's much better that way.


public DocumentBuilderBehavior(IJsonApiContext jsonApiContext, IHttpContextAccessor httpContextAccessor)
{
this._jsonApiContext = jsonApiContext;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this. is unnecessary and isn't used anywhere else in the library

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I'll remove that.


public bool OmitNullValuedAttributes { get; }
public bool AllowClientOverride { get; }
// ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️


namespace JsonApiDotNetCore.Builders
{
public class DocumentBuilderBehavior : IDocumentBuilderBehavior
Copy link
Contributor

@jaredcnance jaredcnance Jan 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class does not define the behavior of the DocumentBuilder but instead instantiates an options class based on config+request data. The "behavior" suffix is confusing especially with the NullAttributeResponseBehavior struct whose purpose more closely matches the suffix. I would recommend renaming this class to DocumentBuilderOptionsProvider, ...Factory, or ...Service

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense. I'll go with DocumentBuilderOptionsProvider.

@jaredcnance jaredcnance changed the title Feat/#226 Feat/#226: Add DocumentBuilderBehavior and DocumentBuilderOptions Jan 29, 2018
@jaredcnance jaredcnance changed the title Feat/#226: Add DocumentBuilderBehavior and DocumentBuilderOptions Feat/#226: Add DocumentBuilderOptions allowing omission of null attributes Jan 29, 2018
… from responses

    - NullAttributeResponseBehavior option for configuration
    - Support for global defaults
    - Support for client override using a query string parameter
@lheiskan
Copy link
Contributor Author

Cleaned up the commits, I can have a look at the end to end test bit. We're currently using an internal branch, so from that perspective no need to back port things, but thanks for the offer :)

@lheiskan
Copy link
Contributor Author

Added few end to end test cases related to the null valued attribute handling.

@jaredcnance jaredcnance mentioned this pull request Jan 29, 2018
Copy link
Contributor

@jaredcnance jaredcnance left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with this. Added item to #223 to document this behavior. Thanks for the PR!

@lheiskan
Copy link
Contributor Author

Nice - thank you for the library!

@jaredcnance jaredcnance merged commit b06ce62 into json-api-dotnet:master Jan 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants