i am writting a code to get all variants from shopify. as shopify has cursor -based pagination, i want to hit the api until i reach last page. i have write below code. please suggest if i am improve this or refactor this code in better way:
public static List<productVariantShopify.ProductVariant> getallProductVariant(long productId, string cursor)
{
List<productVariantShopify.ProductVariant> variantlst = new List<productVariantShopify.ProductVariant>();
if (string.IsNullOrEmpty(cursor))
{
//means first time hit is coming
variantlst = VariantListttttt(productId, cursor, variantlst);
}
return variantlst;
}
private static List<ProductVariant> VariantListttttt(long productId, string cursor, List<ProductVariant> variantlst)
{
var result = GetProductVariant(productId, cursor);
Type myType = result.GetType();
IList<PropertyInfo> props = new List<PropertyInfo>(myType.GetProperties());
var pageInfo = new object();
foreach (PropertyInfo prop in props)
{
if (prop != null && result != null)
{
var propValue = prop.GetValue(result, null);
if (prop.Name == "variantlst")
{
if (propValue != null)
{
var resultLst = (List<ProductVariant>)propValue;
variantlst.AddRange(resultLst);
}
}
else if (prop.Name == "pageInfo")
{
pageInfo = propValue;
}
}
}
PropertyInfo[] propertyInfos = pageInfo.GetType().GetProperties();
if (propertyInfos.Length > 0)
{
var hasNextPage = propertyInfos.Where(x => x.Name.Equals("hasNextPage")).FirstOrDefault();
if (hasNextPage != null)
{
if (Convert.ToBoolean(hasNextPage.GetValue(pageInfo, null)))
{
var cursorProp = pageInfo.GetType().GetProperties().Where(x => x.Name.Equals("endCursor")).FirstOrDefault();
if (cursorProp != null)
{
var cursorValue = Convert.ToString(cursorProp.GetValue(pageInfo, null));
if (cursorValue != null)
{
cursor = cursorValue;
}
variantlst = VariantListttttt(productId, cursor, variantlst);
}
}
}
}
pageInfo = null;
return variantlst;
}
public static object GetProductVariant(long productId, string cursor)
{
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://myshopify.com/admin/api/2023-07/graphql.json")
};
httpClient.DefaultRequestHeaders.Add("X-Shopify-Access-Token", "xxxxx");
object variables = new object();
if (string.IsNullOrEmpty(cursor))
{
variables = new
{
numProducts = 10,
query = "product_id:" + productId
};
}
else
{
variables = new
{
numProducts = 10,
query = "product_id:" + productId,
cursor
};
}
var queryObject = new
{
query = @"query ($numProducts: Int!,$query: String ,$cursor: String){
productVariants(first: $numProducts,query: $query, after: $cursor) {
edges {
cursor
node {
title
id
selectedOptions{
name
value
}
metafields(keys: [
""key0"",
""key1"", ""key2"",
""key3"",""key4""
]
first:5
){
nodes{
namespace
key
value
id
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
",
variables
};
var request = new HttpRequestMessage
{
Method = HttpMethod.Post,
Content = new StringContent(JsonConvert.SerializeObject(queryObject), Encoding.UTF8, "application/json")
};
List<productVariantShopify.ProductVariant> variantlst = new List<productVariantShopify.ProductVariant>();
try
{
using (var response = httpClient.SendAsync(request).GetAwaiter().GetResult())
{
response.EnsureSuccessStatusCode();
var responseString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var result = JsonConvert.DeserializeObject<UpdatedProductVariantModel.Root>(responseString);
//string debugQueryObject = queryObject.query;
//string debugVariableObject = JsonConvert.SerializeObject(queryObject.variables).Replace("\\r", "").Replace("\\n", "");
if (result != null && result.data != null && result.data.productVariants != null && result.data.productVariants.edges != null)
{
fillProdVariantData(variantlst, result);
return new
{
variantlst,
result.data.productVariants.pageInfo
};
}
else
{
if (responseString.Contains("error") && responseString.Contains("Throttled"))
{
return GetProductVariant(productId, string.Empty);
}
}
}
}
catch (Exception ex)
{
if (ex.InnerException != null && ex.InnerException.Message == "")
{
if (ex.InnerException != null && ex.InnerException.Message.Trim() == "The request was aborted: Could not create SSL/TLS secure channel.")
{
return GetProductVariant(productId, string.Empty);
}
}
}
return null;
}
private static List<ProductVariant> fillProdVariantData(List<ProductVariant> variantlst, UpdatedProductVariantModel.Root? result)
{
if (result != null)
{
var variants = result.data.productVariants.edges;
if (variants != null)
{
if (variants.Count > 0)
{
foreach (var edge in variants)
{
var node = edge.node;
string color = string.Empty;
var colorOption1 = node.selectedOptions.Where(x => x.name == "Color").ToList();
if (colorOption1 != null && colorOption1.Count > 0)
{
var colorOption1Value = colorOption1.FirstOrDefault();
if(colorOption1Value != null)
{
color = colorOption1Value.value;
}
}
string size = string.Empty;
var sizeOption2 = node.selectedOptions.Where(x => x.name == "Size").ToList();
if (sizeOption2 != null && sizeOption2.Count > 0)
{
var sizeOption2Value = sizeOption2.FirstOrDefault();
if (sizeOption2Value != null)
{
size = sizeOption2Value.value;
}
}
if (color == "" && size == "")
continue;
var metaFieldID = string.Empty;
var metaFieldKey = string.Empty;
if (node.metafields.nodes != null)
{
List<MetaField> myMetafields = new List<MetaField>();
foreach (var node2 in node.metafields.nodes)
{
var field = node2;
myMetafields.Add(new MetaField { Id = Convert.ToInt64(field.id.Replace("gid://shopify/Metafield/", "")), Key = field.key, Value = field.value });
};
ProductVariant productVariant = new ProductVariant()
{
Id = Convert.ToInt64(node.id.Replace("gid://shopify/ProductVariant/", "")),
Option1 = color,
Option2 = size,
AdminGraphQLAPIId = node.id,
Metafields = myMetafields
};
variantlst.Add(productVariant);
}
else
{
ProductVariant productVariant = new ProductVariant()
{
Id = Convert.ToInt64(node.id.Replace("gid://shopify/ProductVariant/", "")),
Option1 = color,
Option2 = size,
AdminGraphQLAPIId = node.id
};
variantlst.Add(productVariant);
}
}
}
}
}
return variantlst;
}
1 Answer 1
There is an unusual lack of types in this code - it looks like it was originally written in Python or JavaScript and ported to C#. All of the places that you use object
and Reflection would be easier to read and safer to run if you were using types and normal property accessors (getter/setter).
Similarly, detecting exception cases based on the Message strings is not best practice. You should be able to catch based on Exception type, or if there isn't one specific enough, you should be able to check a property like e.g. ErrorCode. Message strings are generally not guaranteed to be stable and can change based on the environment because of localization.
There are chunks of near-identical code to read color and size that could be pulled out into a separate function.
Naming is inconsistent. I assume VariantListttttt
is not serious, but it doesn't match getallProductVariant
either in casing or style. These should probably be GetVariantList
and GetAllProductVariants
, though it's hard to tell from those names what the difference is - can you think of better names? node2
could be childNode
or similar, I dislike adding a number to make a unique name, it doesn't tell you how it's different. Similarly, using "my" as in myMetafields
is a pet peeve of mine - what does that name have over nodefields
?
Why does getallProductVariant
return an empty list if cursor
is not null?
ShopifySharp
library? \$\endgroup\$