Skip to content

Commit

Permalink
feat: add FieldsToNull and IngoreNulls options to control null field …
Browse files Browse the repository at this point in the history
…serialization and to allow explicitly setting fields to null
  • Loading branch information
anthonyreilly committed Jan 26, 2023
1 parent 2e27c64 commit cc4be92
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 35 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

### 2023-01-24 v4.0.0 Beta
### 2023-01-26 v4.0.0 Beta2

* feat: Partial sObjectTree API resource support - added CreateMultipleRecords to create multiple records in a single request
- see https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_composite_sobject_tree_flat.htm
Expand All @@ -11,6 +11,7 @@
* fix: update Newtonsoft.json to v13
* fix: Syntax error in error message generator
* feat: Update ForceClient to use static shared HttpClient by default, unless custom instance is specified
* feat: Add FieldsToNull parameter to updates to allow explicitly setting fields to null. (Note: this requries both setting the value itself to null, in addition to adding the property name to the list)

### 2022-01-13 v3.1.0

Expand Down
2 changes: 1 addition & 1 deletion build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<!--Versioning-->
<VersionPrefix>4.0.0</VersionPrefix>
<VersionSuffix>beta</VersionSuffix>
<VersionSuffix>beta2</VersionSuffix>

<!-- Targets -->
<LangVersion>8.0</LangVersion>
Expand Down
27 changes: 27 additions & 0 deletions src/NetCoreForce.Client.Tests/SerializerTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using NetCoreForce.Client;
using NetCoreForce.Client.Attributes;
using Newtonsoft.Json;
Expand Down Expand Up @@ -108,6 +109,32 @@ public void NullValueHandlingForCreate()
Assert.DoesNotContain("nullProperty", serialized);
}

[Fact]
public void NullValueHandling_WithFieldsToNull_ForUpdate()
{
List<string> fieldsToNull = new List<string>(){ "nullProperty"};
string serialized = JsonSerializer.SerializeForUpdate(new SampleObject(), fieldsToNull);

Assert.Contains("nullProperty", serialized);
}

[Fact]
public void NullValueHandling_WithIgnoreNulls_ForUpdate()
{
string serialized = JsonSerializer.SerializeForUpdate(new SampleObject(), ignoreNulls: false);

Assert.Contains("nullProperty", serialized);
}

[Fact]
public void NullValueHandling_WithFieldsToNull_MixedCase_ForUpdate()
{
List<string> fieldsToNull = new List<string>(){ "NullProPertY"};
string serialized = JsonSerializer.SerializeForUpdate(new SampleObject(), fieldsToNull);

Assert.Contains("nullProperty", serialized);
}

//TODO: test deserialize
}
}
62 changes: 51 additions & 11 deletions src/NetCoreForce.Client/ForceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,16 @@ public async Task<T> GetObjectById<T>(string sObjectTypeName, string objectId, L
/// <param name="sObjectTypeName">SObject name, e.g. "Account"</param>
/// <param name="sObject">Object to create</param>
/// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <returns>CreateResponse object, includes new object's ID</returns>
/// <exception cref="ForceApiException">Thrown when creation fails</exception>
public async Task<CreateResponse> CreateRecord<T>(string sObjectTypeName, T sObject, Dictionary<string, string> customHeaders = null)
public async Task<CreateResponse> CreateRecord<T>(
string sObjectTypeName,
T sObject,
Dictionary<string, string> customHeaders = null,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
Dictionary<string, string> headers = new Dictionary<string, string>();

Expand All @@ -397,7 +404,7 @@ public async Task<CreateResponse> CreateRecord<T>(string sObjectTypeName, T sObj

JsonClient client = new JsonClient(AccessToken, SharedHttpClient);

return await client.HttpPostAsync<CreateResponse>(sObject, uri, headers);
return await client.HttpPostAsync<CreateResponse>(sObject, uri, headers, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}

/// <summary>
Expand All @@ -407,9 +414,17 @@ public async Task<CreateResponse> CreateRecord<T>(string sObjectTypeName, T sObj
/// <param name="sObjects">Objects to create. Each sObject must have the entity type and reference id in the attributes property object.</param>
/// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param>
/// <param name="autoFillAttributes">Automatically create attribute object property, reference Id will be the zero-based index of the array</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <returns>SObjectTreeResponse object, includes new object IDs, and errors if any</returns>
/// <exception cref="ForceApiException">Thrown when creation fails</exception>
public async Task<SObjectTreeResponse> CreateMultipleRecords(string sObjectTypeName, List<SObject> sObjects, bool autoFillAttributes = true, Dictionary<string, string> customHeaders = null)
public async Task<SObjectTreeResponse> CreateMultiple(
string sObjectTypeName,
List<SObject> sObjects,
bool autoFillAttributes = true,
Dictionary<string, string> customHeaders = null,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
if (sObjects == null)
{
Expand Down Expand Up @@ -461,19 +476,28 @@ public async Task<SObjectTreeResponse> CreateMultipleRecords(string sObjectTypeN

SObjectTreeRequest treeRequest = new SObjectTreeRequest(sObjects);

return await client.HttpPostAsync<SObjectTreeResponse>(treeRequest, uri, headers);
return await client.HttpPostAsync<SObjectTreeResponse>(treeRequest, uri, headers, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}

/// <summary>
/// Updates
/// Update single record
/// </summary>
/// <param name="sObjectTypeName">SObject name, e.g. "Account"</param>
/// <param name="objectId">Id of Object to update</param>
/// <param name="sObject">Object to update</param>
/// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <typeparam name="T"></typeparam>
/// <returns>void, API returns 204/NoContent</returns>
/// <exception cref="ForceApiException">Thrown when update fails</exception>
public async Task UpdateRecord<T>(string sObjectTypeName, string objectId, T sObject, Dictionary<string, string> customHeaders = null)
public async Task UpdateRecord<T>(
string sObjectTypeName,
string objectId,
T sObject,
Dictionary<string, string> customHeaders = null,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
Dictionary<string, string> headers = new Dictionary<string, string>();

Expand All @@ -491,7 +515,7 @@ public async Task UpdateRecord<T>(string sObjectTypeName, string objectId, T sOb

JsonClient client = new JsonClient(AccessToken, SharedHttpClient);

await client.HttpPatchAsync<object>(sObject, uri, headers);
await client.HttpPatchAsync<object>(sObject, uri, headers, ignoreNulls: ignoreNulls, fieldsToNull: fieldsToNull);

return;
}
Expand All @@ -506,10 +530,17 @@ public async Task UpdateRecord<T>(string sObjectTypeName, string objectId, T sOb
/// <param name="sObjects">Objects to update</param>
/// <param name="allOrNone">Optional. Indicates whether to roll back the entire request when the update of any object fails (true) or to continue with the independent update of other objects in the request. The default is false.</param>
/// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <returns>List of UpsertResponse objects, includes response for each object (id, success, errors)</returns>
/// <exception cref="ArgumentException">Thrown when missing required information</exception>
/// <exception cref="ForceApiException">Thrown when update fails</exception>
public async Task<List<UpsertResponse>> UpdateRecords(List<SObject> sObjects, bool allOrNone = false, Dictionary<string, string> customHeaders = null)
public async Task<List<UpsertResponse>> UpdateRecords(
List<SObject> sObjects,
bool allOrNone = false,
Dictionary<string, string> customHeaders = null,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
if (sObjects == null)
{
Expand Down Expand Up @@ -542,7 +573,7 @@ public async Task<List<UpsertResponse>> UpdateRecords(List<SObject> sObjects, bo

UpsertRequest upsertRequest = new UpsertRequest(sObjects, allOrNone);

return await client.HttpPatchAsync<List<UpsertResponse>>(upsertRequest, uri, headers, includeSObjectId: true);
return await client.HttpPatchAsync<List<UpsertResponse>>(upsertRequest, uri, headers, includeSObjectId: true, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);

}

Expand All @@ -554,9 +585,18 @@ public async Task<List<UpsertResponse>> UpdateRecords(List<SObject> sObjects, bo
/// <param name="fieldValue">External ID field value</param>
/// <param name="sObject">Object to update</param>
/// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <returns>UpsertResponse object, includes new object's ID if record was created and no value if object was updated</returns>
/// <exception cref="ForceApiException">Thrown when request fails</exception>
public async Task<UpsertResponse> InsertOrUpdateRecord<T>(string sObjectTypeName, string fieldName, string fieldValue, T sObject, Dictionary<string, string> customHeaders = null)
public async Task<UpsertResponse> InsertOrUpdateRecord<T>(
string sObjectTypeName,
string fieldName,
string fieldValue,
T sObject,
Dictionary<string, string> customHeaders = null,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
Dictionary<string, string> headers = new Dictionary<string, string>();

Expand All @@ -574,7 +614,7 @@ public async Task<UpsertResponse> InsertOrUpdateRecord<T>(string sObjectTypeName

JsonClient client = new JsonClient(AccessToken, SharedHttpClient);

return await client.HttpPatchAsync<UpsertResponse>(sObject, uri, headers);
return await client.HttpPatchAsync<UpsertResponse>(sObject, uri, headers, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}

/// <summary>
Expand Down
55 changes: 49 additions & 6 deletions src/NetCoreForce.Client/JsonClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,40 @@ public JsonClient(string accessToken, HttpClient httpClient = null)
}
}

/// <summary>
///
/// </summary>
/// <param name="uri"></param>
/// <param name="customHeaders"></param>
/// <param name="deserializeResponse"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<T> HttpGetAsync<T>(Uri uri, Dictionary<string, string> customHeaders = null, bool deserializeResponse = true)
{
//TODO: can this handle T = string?
return await HttpAsync<T>(uri, HttpMethod.Get, null, customHeaders, deserializeResponse);
}

public async Task<T> HttpPostAsync<T>(object inputObject, Uri uri, Dictionary<string, string> customHeaders = null, bool deserializeResponse = true)
/// <summary>
///
/// </summary>
/// <param name="inputObject"></param>
/// <param name="uri"></param>
/// <param name="customHeaders"></param>
/// <param name="deserializeResponse"></param>
/// <param name="fieldsToNull"></param>
/// <param name="ignoreNulls"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<T> HttpPostAsync<T>(
object inputObject,
Uri uri,
Dictionary<string, string> customHeaders = null,
bool deserializeResponse = true,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
var json = JsonSerializer.SerializeForCreate(inputObject);
var json = JsonSerializer.SerializeForCreate(inputObject, fieldsToNull, ignoreNulls);

var content = new StringContent(json, Encoding.UTF8, JsonMimeType);

Expand All @@ -93,29 +118,47 @@ public async Task<T> HttpPostAsync<T>(object inputObject, Uri uri, Dictionary<st
/// <param name="deserializeResponse"></param>
/// <param name="serializeComplete">Serializes ALL object properties to include in the request, even those not appropriate for some update/patch calls.</param>
/// <param name="includeSObjectId">includes the SObject ID when serializing the request content</param>
/// <param name="fieldsToNull">A list of properties that should be set to null, but inclusing the null values in the serialized output</param>
/// <param name="ignoreNulls">Use with caution. By default null values are not serialized, this will serialize all explicitly nulled or missing properties as null</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<T> HttpPatchAsync<T>(object inputObject, Uri uri, Dictionary<string, string> customHeaders = null, bool deserializeResponse = true, bool serializeComplete = false, bool includeSObjectId = false)
public async Task<T> HttpPatchAsync<T>(
object inputObject,
Uri uri,
Dictionary<string, string> customHeaders = null,
bool deserializeResponse = true,
bool serializeComplete = false,
bool includeSObjectId = false,
List<string> fieldsToNull = null,
bool ignoreNulls = true)
{
string json;
if (serializeComplete)
{
json = JsonSerializer.SerializeComplete(inputObject, false);
json = JsonSerializer.SerializeComplete(inputObject, false, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}
else if (includeSObjectId)
{
json = JsonSerializer.SerializeForUpdateWithObjectId(inputObject);
json = JsonSerializer.SerializeForUpdateWithObjectId(inputObject, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}
else
{
json = JsonSerializer.SerializeForUpdate(inputObject);
json = JsonSerializer.SerializeForUpdate(inputObject, fieldsToNull: fieldsToNull, ignoreNulls: ignoreNulls);
}

var content = new StringContent(json, Encoding.UTF8, JsonMimeType);

return await HttpAsync<T>(uri, new HttpMethod("PATCH"), content, customHeaders, deserializeResponse);
}

/// <summary>
///
/// </summary>
/// <param name="uri"></param>
/// <param name="customHeaders"></param>
/// <param name="deserializeResponse"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public async Task<T> HttpDeleteAsync<T>(Uri uri, Dictionary<string, string> customHeaders = null, bool deserializeResponse = true)
{
HttpRequestMessage request = new HttpRequestMessage();
Expand Down
Loading

0 comments on commit cc4be92

Please sign in to comment.