Skip to content
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

add omit-values supports #657

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

add omit-values supports #657

wants to merge 2 commits into from

Conversation

xuzhg
Copy link
Member

@xuzhg xuzhg commented Aug 8, 2022

OData specs has the following at: http://docs.oasis-open.org/odata/odata/v4.01/cs01/part1-protocol/odata-v4.01-cs01-part1-protocol.html#_Toc505771137

 8.2.8.6 [Preference omit-values](http://docs.oasis-open.org/odata/odata/v4.01/cs01/part1-protocol/odata-v4.01-cs01-part1-protocol.html#sec_Preferenceomitvalues)
The omit-values preference specifies values that MAY be omitted from a response payload. Valid values are nulls or defaults.

If nulls is specified, then the service MAY omit properties containing null values from the response, in which case it MUST specify the Preference-Applied response header with omit-values=nulls.

If defaults is specified, then the service MAY omit properties containing default values from the response, including nulls for properties that have no other defined default value. Nulls MUST be included for properties that have a non-null default value defined. If the service omits default values it MUST specify the Preference-Applied response header with omit-values=defaults.

Properties with null or default values MUST be included in delta payloads, if modified.

The response to a POST operation MUST include any properties not set to their default value, and the response to a PUT/PATCH operation MUST include any properties whose values were changed as part of the operation.

The omit-values preference does not affect a request payload. 

This PR is used to discuss the service side support for omit_values prefer header.

/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance to access.</param>
/// <returns>The <see cref="OmitValuesKind"/> from the request.</returns>
public static OmitValuesKind GetOmitValuesKind(this HttpRequest request)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this indicate that it is a header value, or even a Prefer Header. Something like GetOmitValueHeaderKind

Copy link
Member Author

Choose a reason for hiding this comment

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

Emm, actually it's not only from request header, it's also from ODataOption setting if there's no setting.

But, I am curious which one is a higher priority. @mikepizzo

Copy link
Contributor

Choose a reason for hiding this comment

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

What about GetOmitValuesPreferenceKind, or even just GetOmitValuesPreference with a rename on the enum to OmitValuesPreference?

@@ -71,6 +72,44 @@ public static ODataOptions ODataOptions(this HttpRequest request)
return request.HttpContext.ODataOptions();
}

/// <summary>
/// Returns the <see cref="OmitValuesKind"/> from the request.
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency with the method name, I'd suggest a small rename:

Suggested change
/// Returns the <see cref="OmitValuesKind"/> from the request.
/// Gets the <see cref="OmitValuesKind"/> from the request.

/// Returns the <see cref="OmitValuesKind"/> from the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequest"/> instance to access.</param>
/// <returns>The <see cref="OmitValuesKind"/> from the request.</returns>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also document the exception here?

Suggested change
/// <returns>The <see cref="OmitValuesKind"/> from the request.</returns>
/// <returns>The <see cref="OmitValuesKind"/> from the request.</returns>
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>

/// <returns>The <see cref="OmitValuesKind"/> from the request.</returns>
public static OmitValuesKind GetOmitValuesKind(this HttpRequest request)
{
if (request == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

For constant comparisons, I'd suggest using constant pattern matching on new code:

Suggested change
if (request == null)
if (request is null)

throw Error.ArgumentNull(nameof(request));
}

// The 'Prefer' header from client has the top priority
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of having code blocks with comments such as this, I'd suggest extracting each "strategy" into at least a local function, and then calling them in the explicit fallback order, something like:

return GetOmitValuePreferenceFromHeaders() ?? 
    GetOmitValuePreferenceFromODataConfiguration();

This not only cleanly isolates each "strategy" into a named method (most likely removing the need for the comment itself), but also models the fallback logic in a more clean and direct way that is more apparent to the reader.

/// <summary>
/// Not set, unknown
/// </summary>
Unknown,
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you considered just not having this entry at all? Since it is completely invalid/not part of the spec, I don't think it is a good idea to even map it.

If there are any places where the value is optional, a nullable OmitValuesKind could be used.

prefer_applied = values.FirstOrDefault();
}

string omitValuesHead = omitValuesKind == OmitValuesKind.Nulls ?
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
string omitValuesHead = omitValuesKind == OmitValuesKind.Nulls ?
string omitValuesHeaderValue = omitValuesKind == OmitValuesKind.Nulls ?


string omitValuesHead = omitValuesKind == OmitValuesKind.Nulls ?
"omit-values=nulls" :
"omit-values=defaults";
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of handling these with raw strings, have you considered creating a small model and/or factory method for it?

Imagine something like

public sealed class PreferHeader
{
    public static PreferHeader OmitValues(OmitValuesKind kind)
    {
        return new("omit-values", kind.ToText())
    }
}

This would remove the need to ever handle raw strings. You can parse values into this model and use factory methods to create strongly typed instances for each preference OData has.

}
else
{
response.Headers[PreferAppliedHeaderName] = $"{prefer_applied},{omitValuesHead}";
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way to avoid this manual string concatenation by relying on the StringValues type itself to do it?

We could probably "add" the omit setting to the existing StringValues instance and then write that directly.

Comment on lines +37 to +38
// If there are many "Preference-Applied" headers, pick up the first one.
prefer_applied = values.FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you elaborate why we are picking the first one? Shouldn't we just append ours to the end of the existing list no matter how many there are?

/// <summary>
/// Gets or sets a omit values kind for the property value serialization.
/// </summary>
public OmitValuesKind OmitValuesKind { get; set; } = OmitValuesKind.Unknown;
Copy link
Contributor

Choose a reason for hiding this comment

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

As per other comments, I think it would be better if this was a nullable:

Suggested change
public OmitValuesKind OmitValuesKind { get; set; } = OmitValuesKind.Unknown;
public OmitValuesKind? OmitValuesKind { get; set; };

Comment on lines +92 to +100
if (preferHeader.Contains("omit-values=nulls", StringComparison.OrdinalIgnoreCase))
{
return OmitValuesKind.Nulls;
}

if (preferHeader.Contains("omit-values=defaults", StringComparison.OrdinalIgnoreCase))
{
return OmitValuesKind.Defaults;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a possibility for both 'omit-values=defaults' and 'omit-values=nulls' to be added as headers in a request? In that case won't one of them be ignored?

Copy link
Contributor

@Nthemba Nthemba left a comment

Choose a reason for hiding this comment

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

Add some tests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants