Overview
In many data models, a single field is not always sufficient to uniquely identify a record. A composite primary key, which combines two or more columns, is often required. This document outlines a strategy for handling entities that can be identified by either a simple, single-value ID or a composite key.
The example focuses on a ProductVariant
object in a Shopify context, but it can be adapted and used with other integrations. A variant can be uniquely identified by its own VariantId
, or by a composite key that combines the ProductId
and the VariantId
. This approach provides flexibility in how objects are referenced and updated throughout an application.
Problem/Challenge
Application code often needs to retrieve or update specific records. When a record can be identified in multiple ways (e.g., a simple VariantId
vs. a composite ProductId\VariantId
), the client-side code is forced to handle this complexity. This can lead to inconsistent logic, duplicated code, and a higher likelihood of errors if the identification logic is not handled uniformly. The challenge is to create a data model that can gracefully accept different identification formats without burdening the model's consumer with the underlying parsing and validation logic.
Solution Design
The proposed solution centralizes the ID management logic within the data model itself, specifically in the SetPrimaryId
method. This method acts as a single, intelligent entry point for setting the object's identifier.
Unified ID Setter: A single method,
SetPrimaryId
, is responsible for handling all incoming ID formats.Defined Composite Key Format: A clear and parsable format for composite keys is established. In this example, a backslash (
\
) separates theProductId
andVariantId
(e.g.,"12345\67890"
). This delimiter makes the relationship between the two parts explicit.Intelligent Format Detection: The
SetPrimaryId
method inspects the incoming ID value to determine if it is a simple or composite key. It automatically parses the components if a composite key is detected.Graceful Error Handling: The implementation validates the format of the ID and the data types of its components. If the input is invalid (e.g., wrong number of parts, non-numeric values), it invokes a dedicated
HandleInvalidPrimaryId
method instead of throwing an unhandled exception, ensuring data integrity and predictable failure behavior.
Benefits
Improved Flexibility: Allows developers to identify objects using the most convenient method for their specific context, whether it's a direct
VariantId
or a composite key.Strong Encapsulation: Hides the complexity of parsing and validation from the client code. Consumers of the
ProductVariant
object can callSetPrimaryId
without needing to know the internal rules.Increased Robustness: By validating formats and data types when setting the ID, the model is protected from storing corrupted or invalid data, resulting in fewer runtime errors.
Enhanced Maintainability: If the rules for identification change in the future (e.g., a different delimiter), the logic only needs to be updated in one centralized location (
SetPrimaryId
), preventing breaking changes across the application.Backward Compatibility: The design supports both legacy simple IDs and the newer composite format, allowing for a smoother evolution of the system over time.
Implementation Notes
The
GetPrimaryId
method consistently returns the variant's ownId
, even if theProductId
is available. This provides a predictable and stable "primary" representation of the object's identity.The
SetPrimaryId
method accepts aThrowErrorOnInvalid
flag, which allows the caller to control whether an invalid ID format should throw an exception or fail silently. This is useful for scenarios where immediate failure is required versus those where it is not.In the provided C# example, the composite key parts are parsed as integers, while the simple key is parsed as a long. This accommodates different numeric ranges for different ID types.
Example of advanced GetPrimaryId/SetPrimaryId
/*
* Example of advanced GetPrimaryId / SetPrimaryId from shopify product variant
*/
/// <summary>
/// Gets the primary ID for the product variant.
/// </summary>
/// <returns>The primary ID as an object, which is the variant's own ID.</returns>
public override object GetPrimaryId()
{
// If ProductId is present, this logic still returns the variant's own Id.
if (ProductId != null)
{
return Convert.ToString(Id);
}
// In all cases, return the variant's Id.
return Convert.ToString(Id);
}
/// <summary>
/// Sets the primary ID for the product variant. It can handle both simple and composite IDs.
/// A composite ID is expected to be in the format "ProductId\Id".
/// </summary>
/// <param name="primaryId">The primary ID string to set.</param>
/// <param name="ThrowErrorOnInvalid">Flag to indicate if an error should be thrown for an invalid ID.</param>
public override void SetPrimaryId(string primaryId, bool ThrowErrorOnInvalid = false)
{
// If the provided primaryId is null, handle the invalid ID and exit.
if (primaryId == null)
{
HandleInvalidPrimaryId(primaryId, ThrowErrorOnInvalid, "ProductVariant");
return;
}
int CustomerId_value;
// Check if the primaryId is a composite key by looking for a backslash.
if (primaryId.Contains("\\"))
{
// Split the composite key into its parts.
string[] ids = primaryId.Split('\\');
// A valid composite key must have exactly two parts.
if (ids.Length != 2)
{
HandleInvalidPrimaryId(primaryId, ThrowErrorOnInvalid, "ProductVariant");
return;
}
// Try to parse the first part as the ProductId.
if (!int.TryParse(ids[0], out CustomerId_value))
{
// If parsing fails, handle the invalid ID and exit.
HandleInvalidPrimaryId(primaryId, ThrowErrorOnInvalid, "ProductVariant", "ProductId");
return;
}
int Id_value;
// Try to parse the second part as the variant Id.
if (!int.TryParse(ids[1], out Id_value))
{
// If parsing fails, handle the invalid ID and exit.
HandleInvalidPrimaryId(primaryId, ThrowErrorOnInvalid, "ProductVariant", "Id");
return;
}
// If both parts are valid, assign them to the respective properties.
ProductId = CustomerId_value.ToString();
Id = Id_value.ToString();
}
else
{
// If it's not a composite key, treat it as a simple ID.
long Id_value;
// Try to parse the primaryId as a long integer.
if (!long.TryParse(primaryId, out Id_value))
// If parsing fails, handle the invalid ID.
HandleInvalidPrimaryId(primaryId, ThrowErrorOnInvalid, "ProductVariant", "Id");
else
// If parsing succeeds, assign it to the Id property.
Id = Id_value.ToString();
}
}