shopifysharp-skill
Integrating Shopify with a .NET/C# backend using the ShopifySharp library. Use when configuring API keys, webhooks, rate limits, DI, GraphQL with IGraphService, or handling any Shopify REST/GraphQL resources to prevent common production bugs.
ShopifySharp Skill
Library: ShopifySharp >= 6.26.0
Target API Version: Shopify 2026-01 (latest stable)
Target Framework: .NET 8 / .NET 9
1. Installation
dotnet add package ShopifySharp
dotnet add package ShopifySharp.Extensions.DependencyInjection
Use 6.26.0 or higher — minimum required for Shopify API
2026-01. Reference: https://www.nuget.org/packages/ShopifySharp
2. Environment Variables and Config
Required Variables
SHOPIFY_API_KEY=your_api_key
SHOPIFY_API_SECRET=your_api_secret
SHOPIFY_SHOP=your-store.myshopify.com
SHOPIFY_ACCESS_TOKEN=shpat_xxxxxxxxxxxx
Never hardcode credentials. Always load them from
IConfiguration.
Typed Options Class
// Infrastructure/ShopifyOptions.cs
public class ShopifyOptions
{
public string ApiKey { get; set; } = string.Empty;
public string ApiSecret { get; set; } = string.Empty;
public string Shop { get; set; } = string.Empty;
public string AccessToken { get; set; } = string.Empty;
}
appsettings.json
{
"Shopify": {
"ApiKey": "",
"ApiSecret": "",
"Shop": "your-store.myshopify.com",
"AccessToken": "shpat_xxxxxxxxxxxx"
}
}
3. API Version Configuration
Always enforce the API version. Without this, ShopifySharp might default to an obsolete/unsupported version.
// Via DI options (preferred — see section 4)
builder.Services.AddShopifySharp<LeakyBucketExecutionPolicy>(options =>
{
options.ApiVersion = "2026-01";
});
// Global fallback (useful outside DI or in scripts)
ShopifySharp.Infrastructure.ShopifyService.SetGlobalApiVersion("2026-01");
4. Dependency Injection — Extension Method Pattern
The Program.cs Bloat Problem
Registering each service individually with factory lambdas bloats Program.cs with repetitive code. The solution is a centralized extension method.
AddShopifyServices() — Recommended Pattern
// Infrastructure/ShopifyServiceExtensions.cs
using ShopifySharp;
using ShopifySharp.Extensions.DependencyInjection;
public static class ShopifyServiceExtensions
{
public static IServiceCollection AddShopifyServices(
this IServiceCollection services,
IConfiguration configuration)
{
// 1. Bind config
services.Configure<ShopifyOptions>(
configuration.GetSection("Shopify"));
// 2. Core: rate limiting + API version
services.AddShopifySharp<LeakyBucketExecutionPolicy>(options =>
{
options.ApiVersion = "2026-01";
});
// 3. Register the required services for the project
services.AddScoped<IProductService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new ProductService(o.Shop, o.AccessToken);
});
services.AddScoped<IOrderService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new OrderService(o.Shop, o.AccessToken);
});
services.AddScoped<ICustomerService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new CustomerService(o.Shop, o.AccessToken);
});
services.AddScoped<IInventoryLevelService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new InventoryLevelService(o.Shop, o.AccessToken);
});
services.AddScoped<IWebhookService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new WebhookService(o.Shop, o.AccessToken);
});
services.AddScoped<IMetafieldService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new MetafieldService(o.Shop, o.AccessToken);
});
// GraphQL (see section 8)
services.AddScoped<IGraphService>(sp =>
{
var o = sp.GetRequiredService<IOptions<ShopifyOptions>>().Value;
return new GraphService(o.Shop, o.AccessToken);
});
return services;
}
}
Resulting Program.cs — One single line
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddShopifyServices(builder.Configuration); // Includes all Shopify configuration
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
See
references/services-catalog.mdfor the entire catalog of available services.
5. Authentication and Webhook Validation
Direct Instantiation (Without DI)
var productService = new ProductService("your-store.myshopify.com", "shpat_xxx");
CRITICAL: HMAC Webhook Validation in ASP.NET Core
The bug: Request.Body is a forward-only stream. If something reads it first (model binder, middleware), the HMAC calculation receives an empty stream — or vice versa. The signature validation will always fail.
The solution: Request.EnableBuffering() converts the stream to seekable, allowing multiple reads and resetting with Position = 0.
// Controllers/WebhooksController.cs
[ApiController]
[Route("api/webhooks")]
public class WebhooksController : ControllerBase
{
private readonly IOptions<ShopifyOptions> _opts;
private readonly ILogger<WebhooksController> _logger;
public WebhooksController(IOptions<ShopifyOptions> opts, ILogger<WebhooksController> logger)
{
_opts = opts;
_logger = logger;
}
[HttpPost("orders/create")]
public async Task<IActionResult> OrdersCreate()
{
// STEP 1: Enable buffering BEFORE any stream reading
Request.EnableBuffering();
// STEP 2: Read the raw body to calculate HMAC
string rawBody;
using (var reader = new StreamReader(
Request.Body,
encoding: System.Text.Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
leaveOpen: true)) // leaveOpen:true prevents closing the underlying stream
{
rawBody = await reader.ReadToEndAsync();
}
// STEP 3: Reset position — the model binder/deserializer can read it now
Request.Body.Position = 0;
// STEP 4: Validate HMAC with rawBody
bool isValid = AuthorizationService.IsAuthenticWebhook(
requestHeaders: Request.Headers,
requestBody: rawBody,
shopifyApiSecret: _opts.Value.ApiSecret
);
if (!isValid)
{
_logger.LogWarning("Invalid Webhook HMAC — possible unauthorized request");
return Unauthorized();
}
// STEP 5: Safely deserialize (stream is already reset)
var order = System.Text.Json.JsonSerializer.Deserialize<Order>(rawBody);
if (order is null) return BadRequest("Invalid payload");
_logger.LogInformation("Order received: {OrderId}", order.Id);
// ... process order
return Ok();
}
}
Why leaveOpen: true: Without this flag, disposing the StreamReader closes the underlying HTTP request stream, and resetting Position = 0 will throw an ObjectDisposedException.
Alternative Middleware (Multiple Webhook Endpoints)
// Program.cs — register before app.MapControllers()
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/webhooks"))
context.Request.EnableBuffering();
await next();
});
6. Rate Limiting — LeakyBucketExecutionPolicy
Shopify applies limits: REST ~2 req/s (leaky bucket), GraphQL based on query cost.
// Automatically configured within AddShopifyServices() from Step 4.
// For direct usage outside of DI:
var policy = new LeakyBucketExecutionPolicy();
var service = new ProductService(shop, token);
service.SetExecutionPolicy(policy);
Real Policy Behavior — Critical Accuracy
LeakyBucketExecutionPolicy manages retries transparently and automatically. However:
- When Shopify responds
429, the policy waits and retries without throwing an exception. - If retries are exhausted (timeout or max attempts reached), it throws
ShopifyRateLimitException. ShopifyRateLimitExceptioninherits fromShopifyException— it must always be caught.- The policy does NOT suppress the definitive exception — it just delays it as much as possible.
// Correct: catch ShopifyRateLimitException as an explicit fallback
try
{
await productService.CreateAsync(product);
}
catch (ShopifyRateLimitException ex)
{
// Retries exhausted — escalate, queue for deferred retry, or return 429
logger.LogError("Rate limit exhausted after retries. RetryAfter: {s}s",
ex.RetryAfterSeconds);
// Example: throw new TooManyRequestsException(ex.RetryAfterSeconds);
}
catch (ShopifyException ex)
{
logger.LogError(ex, "Shopify API Error [{StatusCode}]", ex.HttpStatusCode);
}
In high-volume scenarios (bulk imports, background jobs making thousands of calls),
ShopifyRateLimitExceptionis likely to occur even if the policy is active. Never assume the policy absorbs everything.
7. Common Endpoint Patterns (REST)
Products
var page = await productService.ListAsync(new ProductListFilter
{
Limit = 250,
Fields = "id,title,variants,images,status"
});
var product = await productService.GetAsync(productId);
var created = await productService.CreateAsync(new Product
{
Title = "My Product",
Vendor = "My Brand",
ProductType = "Apparel",
Variants = [new ProductVariant { Price = 29.99m, Sku = "SKU-001" }]
});
await productService.UpdateAsync(productId, new Product { Title = "Updated" });
await productService.DeleteAsync(productId);
Orders
var orders = await orderService.ListAsync(new OrderListFilter
{
Status = "open",
Limit = 50
});
var order = await orderService.GetAsync(orderId, "id,email,line_items,financial_status");
await orderService.CloseAsync(orderId);
Customers
var customer = await customerService.GetAsync(customerId);
var results = await customerService.SearchAsync(new CustomerSearchListFilter
{
Query = "email:[email protected]"
});
Inventory
await inventoryService.SetAsync(new InventoryLevel
{
LocationId = locationId,
InventoryItemId = inventoryItemId,
Available = 100
});
Metafields
await metafieldService.CreateAsync(new Metafield
{
Namespace = "custom",
Key = "color",
Value = "blue",
Type = "single_line_text_field",
OwnerId = productId,
OwnerResource = "product"
});
8. GraphQL with IGraphService
Shopify is actively migrating critical resources to become GraphQL-only. IGraphService is the standard for:
- High-volume operations (bulk operations)
- Complex queries or joins between resources that REST does not support well
- Resources already deprecated or removed from the REST API in
2026-01
// Inject IGraphService (registered in AddShopifyServices)
public class ProductGraphRepository
{
private readonly IGraphService _graph;
public ProductGraphRepository(IGraphService graph) => _graph = graph;
// Basic query
public async Task<string> GetProductAsync(string gid)
{
var query = """
query GetProduct($id: ID!) {
product(id: $id) {
id
title
status
totalInventory
variants(first: 10) {
edges { node { sku price inventoryQuantity } }
}
}
}
""";
return await _graph.PostAsync(query, new { id = gid });
// Returns a JSON string — deserialize with System.Text.Json as needed
}
// Bulk Operation — export all products (>10k records)
public async Task<string> StartBulkExportAsync()
{
var mutation = """
mutation {
bulkOperationRunQuery(
query: "{ products { edges { node { id title variants { edges { node { sku price } } } } } } }"
) {
bulkOperation { id status }
userErrors { field message }
}
}
""";
return await _graph.PostAsync(mutation);
}
}
When to use REST vs GraphQL
| Scenario | Use |
|---|---|
| Simple CRUD of products/orders/customers | REST (IProductService, etc.) |
| Complex filtering or resource joins | GraphQL (IGraphService) |
| Exporting >10,000 records | GraphQL Bulk Operations |
Resources deprecated in REST 2026-01 | GraphQL (Mandatory) |
| Simple Mutations (create, update) | REST (More straightforward) |
For comprehensive GraphQL queries, mutations, pagination, and bulk result handling, read
references/graphql.md.
9. Cursor-based Pagination
// Correctly iterate through ALL pages
string? pageInfo = null;
do
{
var filter = new ProductListFilter { Limit = 250, PageInfo = pageInfo };
var page = await productService.ListAsync(filter);
foreach (var product in page.Items)
{
// process
}
pageInfo = page.GetNextPageFilter()?.PageInfo;
} while (pageInfo is not null);
Do not use the
pageparameter (deprecated since API 2019-10). Always usePageInfo.
10. Comprehensive Error Handling
try
{
var product = await productService.GetAsync(productId);
}
catch (ShopifyException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound)
{
logger.LogWarning("Resource not found: {Id}", productId);
}
catch (ShopifyRateLimitException ex)
{
// LeakyBucketExecutionPolicy retries automatically, but this exception
// is thrown when all retries are exhausted. Handle it explicitly.
logger.LogError("Rate limit exhausted. RetryAfter: {s}s", ex.RetryAfterSeconds);
}
catch (ShopifyException ex)
{
logger.LogError(ex, "Shopify API Error [{StatusCode}]: {Message}",
ex.HttpStatusCode, ex.Message);
}
11. Best Practices
| Do | Avoid |
|---|---|
Enforce ApiVersion = "2026-01" in DI | Omitting the API version |
Centralize DI inside AddShopifyServices() | Registering services one by one in Program.cs |
Request.EnableBuffering() + Position = 0 in webhooks | Reading Request.Body without buffering |
leaveOpen: true in webhook's StreamReader | Letting the reader close the stream |
Use LeakyBucketExecutionPolicy | Implementing manual Thread.Sleep / Task.Delay |
Catch ShopifyRateLimitException as a fallback | Assuming the policy never throws exceptions |
Load credentials from IConfiguration | Hardcoding API keys |
Utilize Cursor-based pagination (PageInfo) | Paginating with the page param (deprecated) |
Request only necessary fields (Fields = "...") | Always fetching all fields |
Inject interfaces (IProductService) | Injecting concrete classes (ProductService) |
| Validate HMAC upon every webhook received | Trusting payloads without validation |
| Use GraphQL for bulk operations and complex queries | Forcing REST for massive operations |
12. Reference Files
references/services-catalog.md— Comprehensive catalog of services, interfaces, and filter properties.references/graphql.md— GraphQL queries, mutations, bulk operations, and pagination withIGraphService.
Load references/graphql.md when the user asks about GraphQL, bulk exports, or resources removed from the REST API.