Update One Field Not the Whole Entity: Safe Backfills in Sitecore Content Hub
Hello everyone, and welcome to another blog post! In this blog, we’ll explore an important aspect of saving entities in Sitecore Content Hub, particularly during large-scale data update operations such as **backfill activities**, which are a common requirement in Content Hub implementations.
Backfilling a Single Field in Sitecore Content Hub Without Fighting Required Fields
If you've ever run a backfill in Sitecore Content Hub — "populate this one new property across tens of thousands of entities" — you've probably hit the wall this post is about. The task sounds trivial: load the entity, set the value, save. But the moment your entity definition has required members, and especially when some of those required members carry default values you were never told about, the "obvious" save path starts throwing errors, and you're left wondering why updating one harmless field is so hard.
This is the approach that actually works: skip SaveAsync entirely and issue a scoped, raw HTTP PUT that touches exactly one member and nothing else.
Why the SDK save keeps failing
The intuitive pattern looks like this, and it's what most examples show:
var loadConfig = new EntityLoadConfiguration(
CultureLoadOption.Default,
new PropertyLoadOption("MyNewField"),
RelationLoadOption.None);
var entity = await MClient.Entities.GetAsync(id, loadConfig);
entity.SetPropertyValue("MyNewField", value);
await MClient.Entities.SaveAsync(entity);
It looks safe — you only loaded one field, you only changed one field. But SaveAsync still submits the entity through the standard save pipeline, and that pipeline validates the entity against its entire definition. If the definition has required members that aren't part of your minimal load, or required members whose defaults don't line up with what the server expects at save time, validation rejects the write. You didn't come to touch those fields, but the save makes you answer for them anyway.
That's the core problem with a backfill against a schema you don't fully own: a full save is a statement about the whole record, and you're on the hook for every required member on it — including the ones with defaults you can't see.
The approach that works: a scoped raw PUT
Instead of asking the SDK to save the entity, we call the REST endpoint directly and scope the update to a single member using the members query parameter. Content Hub then only considers that one member for the write and its validation — every other required field is left completely alone.
private async Task SavePartialCountAsync(IEntity entity, ILogger log)
{
var countValue = entity.GetPropertyValue<int?>(CountPropertyName);
var entityLink = await _mClient.LinkHelper.EntityToLinkAsync(entity.Id.Value).ConfigureAwait(false);
var definitionLink = await _mClient.LinkHelper.DefinitionToLinkAsync(DefinitionName).ConfigureAwait(false);
var uri = new UriBuilder(entityLink.Uri)
{
Query = $"members={CountPropertyName}"
}.Uri;
var payload = new
{
entitydefinition = new { href = definitionLink.Uri.ToString() },
properties = new Dictionary<string, object>
{
[CountPropertyName] = countValue
}
};
log.LogInformation(
"SavePartialCountAsync: PUT {Uri} for entity {Id} -- entitydefinition={Definition}, {Property}={Value}",
uri, entity.Id, definitionLink.Uri, CountPropertyName, countValue);
using var response = await _mClient.Raw.PutAsync(uri, new JsonContent(payload)).ConfigureAwait(false);
log.LogInformation(
"SavePartialCountAsync: entity {Id} -- response {StatusCode} ({ReasonPhrase})",
entity.Id, (int)response.StatusCode, response.ReasonPhrase);
response.EnsureSuccessStatusCode();
}
What each piece is doing
- Building the links, not hardcoding URLs.
LinkHelper.EntityToLinkAsyncandLinkHelper.DefinitionToLinkAsyncresolve the correct entity and definition URIs from the SDK, so you're never gluing together REST paths by hand. - The
membersquery parameter is the whole trick.?members=CountPropertytells Content Hub to treat this PUT as a partial update limited to that member. Because the request is scoped, validation and persistence apply to that field only — the required fields with mystery defaults never enter the picture. - A minimal payload. The body carries just two things: the
entitydefinitionhref (so the endpoint can resolve the definition) and apropertiesdictionary containing only the one property you're writing. Nothing else is sent, so nothing else can be overwritten. - The raw client.
_mClient.Raw.PutAsyncsends the request through the SDK's underlying HTTP client, which means you still inherit its authentication, throttling, and retry behavior — you're just bypassing the entity save pipeline. - Fail loudly.
EnsureSuccessStatusCode()turns any non-2xx response into an exception, and the surrounding logs record the URI, the value written, and the status code, which is exactly what you want when a backfill is chewing through thousands of records.
Why this is the right call for backfills
Beyond simply not failing, the scoped PUT gives you several things a full save can't:
- No accidental resets. You physically cannot clobber a required field's existing value or trip its default, because that field is never in the request. This is the single biggest risk in a backfill against an unfamiliar schema, and this approach removes it entirely.
- No unknown-field homework. You don't have to discover, load, or reason about every required member and its default. If it's not the field you came for, it's not your problem.
- Fewer triggered side effects. Save-driven triggers, scripts, and actions in Content Hub often react to which members changed. A scoped write keeps that change set honest, so you don't fire downstream logic for fields you never actually modified.
- Cleaner audit trail. The history shows exactly one field changing, on one date, by one integration user — instead of your backfill smearing itself across the audit log of every property on the entity.
- Leaner requests at scale. Content Hub throttles at roughly 15 calls per second per integration user (you'll see HTTP 429 past that), and small, single-member payloads are cheaper to build and process across tens of thousands of entities.
Takeaway
A backfill should be surgical: change one field, leave everything else untouched. SaveAsync is the opposite of surgical — it makes you accountable for the entire entity, required defaults and all. A scoped raw PUT with members= flips that around: you take responsibility for exactly one member and let the server keep owning the rest. For a schema you don't fully know, that's not just a workaround — it's the safer design.
Thanks for reading and keep learning!!
You can check my other blogs too if interested. Blog Website

Comments
Post a Comment