ShopifySharp

repository·master·Indexed 21 days ago

https://github.com/nozzlegear/shopifysharp

A .NET library for building custom Shopify applications using C#, .NET, and GraphQL. It simplifies development by handling authentication and validation, and provides a Dependency Injection extension for integrating services into the .NET DI container. The library includes a GraphQL Parser pipeline that transforms Shopify GraphQL SDL files into C# models and fluent query builders.

Tokens
3.8K
Snippets
11
Records
17
Agent score
75%

What's inside ShopifySharp

  1. How the ShopifySharp GraphQL Parser pipeline works

    master

    The ShopifySharp.GraphQL.Parser project follows a multi-phase pipeline to transform a raw GraphQL schema into C# models and fluent query builders.

    The Processing Pipeline

    1. Parsing & AST Mapping: The entrypoint Parser.ParseAndWriteAsync uses GraphQLParser.Parser to create a GraphQLDocument AST. A custom Visitor traverses this AST and maps GraphQL definitions to internal structures via AstNodeMapper.fs:

      • GraphQLObjectTypeDefinition $\rightarrow$ Class
      • GraphQLInterfaceTypeDefinition $\rightarrow$ Interface
      • GraphQLEnumTypeDefinition $\rightarrow$ VisitedEnum
      • GraphQLInputObjectTypeDefinition $\rightarrow$ InputObject
      • GraphQLUnionTypeDefinition $\rightarrow$ UnionType These are stored in a ParserContext for later use.
    2. Reachability Analysis: To avoid generating dead code, the ReachabilityAnalyzer starts at the QueryRoot and Mutation operations. It recursively discovers all referenced return types, argument types, interfaces, and union cases. Only types tracked by the TypeReferenceTracker proceed to code generation.

    3. Code Generation: Two parallel processes generate code:

      • Model Generation (VisitedTypeWriter.fs): Creates C# record, interface, and enum representations of reachable types, including necessary serialization attributes like [JsonPropertyName], [JsonPolymorphic], and [JsonDerivedType].
      • Query Builder Generation (QueryBuilderWriter.fs): Generates fluent query builders. This includes field builders, argument builders, and specialized builders for unions (UnionsBuilderWriter.fs) and interfaces (InterfacesBuilderWriter.fs) to handle inline fragment selections (e.g., .OnSomeUnionType(...)).
    4. Roslyn Splitting & File Writing: The generated output is passed to the FileSystem.fs module. It uses Roslyn's CSharpSyntaxTree API to parse the entire output, extract individual BaseTypeDeclarationSyntax items, and split them into separate .generated.cs files organized by namespace-mapped subdirectories.

  2. Define properties in new Models

    master

    When creating new model classes, all properties must be nullable wherever possible.

    This is required to prevent unintended serialization of C# default values. For example, a non-nullable bool property like Published will default to false during JSON serialization, which could accidentally unpublish an object if the property was not explicitly set during an update.

  3. Understand the ShopifySharp GraphQL Parser pipeline

    master

    The ShopifySharp.GraphQL.Parser is an F#-based tool that transforms a Shopify GraphQL Schema Definition Language (SDL) file into C# code. The process follows a five-phase pipeline:

    1. Parsing & AST Mapping: The GraphQL schema is parsed into an Abstract Syntax Tree (AST), which is then traversed by a Visitor and mapped into internal domain models (Classes, Interfaces, Enums, etc.) stored in a ParserContext.
    2. Model Generation: The parser iterates through all visited types in the context and writes C# models (records, enums, etc.) to a code pipe.
    3. Reachability Analysis: To avoid generating unnecessary code, the parser performs reachability analysis. It starts from the root fields of Query and Mutation and recurses through fields and arguments to identify which types are actually reachable.
    4. Query Builder Generation: The parser generates fluent query and operation builders for the reachable types. This includes specialized writers for fields, unions, interfaces, and arguments.
    5. Roslyn Splitting & File IO: The unified C# code strings are passed to the Roslyn C# compiler to parse the syntax trees. The parser then splits the large code blocks into individual .generated.cs files based on type declarations and writes them to the specified destination directories.
  4. Migrate from REST to GraphQL using GraphService

    master
    If you are currently using ShopifySharp's REST implementation and wish to migrate to Shopify's GraphQL API, you should use the GraphService. This service allows you to send GraphQL queries and mutations to Shopify. Detailed migration guidance is available in the project wiki.
  5. Install ShopifySharp via dotnet CLI

    master

    You can install the core ShopifySharp library or its Dependency Injection extensions using the dotnet command line.

    • Use ShopifySharp for the base library functionality.
    • Use ShopifySharp.Extensions.DependencyInjection to integrate the library with the .NET Dependency Injection container.
    # Install the core library
    dotnet add package shopifysharp
    
    # Install the Dependency Injection extensions
    dotnet add package shopifysharp.extensions.dependencyinjection
  6. Implement a new ShopifySharp Service

    master

    A 'Service' in ShopifySharp is a class containing methods for interacting with a specific Shopify API endpoint or object type (e.g., CustomerService).

    When implementing a new service, follow these requirements:

    1. Interface: Every service must have an accompanying interface (e.g., ICustomerService) in a separate file.
    2. Documentation: Place all method documentation on the interface methods, not the service class methods.
    3. Pagination: If the service uses a paginated list endpoint, you must implement two ListAsync methods:
      • One using the generic ListFilter<EntityType>.
      • One using a dedicated EntityTypeListFilter.
    4. Testing: New services must include tests, even if they cannot be fully executed due to permission or account requirements.
    using ShopifySharp.Filters;
    
    public class CustomerService : ShopifyService, ICustomerService
    {
        // ...
    
        public virtual async Task<ListResult<Customer>> ListAsync(ListFilter<Customer> filter = null, CancellationToken cancellationToken = default) =>
            await ExecuteGetListAsync("customers.json", "customers", filter, cancellationToken);
    
        public virtual async Task<ListResult<Customer>> ListAsync(CustomerListFilter filter, CancellationToken cancellationToken = default) =>
            await ListAsync(filter?.AsListFilter(), cancellationToken);
    }
  7. Set up environment variables for testing

    master

    To run the ShopifySharp test suite, create a file named _env.yml in the ShopifySharp.Tests project folder. Populate it with the following environment variables.

    Warning: Use an access token from a development store only. Do not use tokens from production or client stores, as the tests perform destructive actions (creating, updating, deleting) on the store.

    SHOPIFYSHARP_MY_SHOPIFY_URL = example.myshopify.com
    
    SHOPIFYSHARP_API_KEY = value
    
    SHOPIFYSHARP_SECRET_KEY = value
    
    # If using a custom app, the custom app's "password" goes here
    SHOPIFYSHARP_ACCESS_TOKEN = value
    
    # Optional, only necessary if you're testing the multipass service
    SHOPIFYSHARP_MULTIPASS_SECRET = value
  8. Run ShopifySharp tests via dotnet CLI

    master

    Tests are implemented using xUnit. You can run the entire suite or filter by specific categories (e.g., Order, Customer) to save time and avoid rate limits.

    # Run all tests in the solution (can take 15+ minutes)
    dotnet test --framework net10.0 ShopifySharp.Tests
    
    # Run tests for a specific category (recommended for development)
    dotnet test --framework net10.0 --filter "Category=Order"
  9. Consume ShopifySharp services via constructor injection

    master

    Once registered in the DI container, you can use ShopifySharp service factories by adding their interfaces to your class constructors.

    When you use a factory to create a service (e.g., orderServiceFactory.Create(credentials)), the resulting service instance will automatically inherit the IRequestExecutionPolicy configured in your DI container. For example, using LeakyBucketExecutionPolicy ensures the service gracefully handles Shopify's API rate limits by waiting instead of throwing exceptions.

    // In the class where you want to use a ShopifySharp service
    public class MyClass(IOrderServiceFactory orderServiceFactory)
    {
        public async Task ListOrdersForUser()
        {
            var user = await DoSomethingToGetUser();
            var credentials = new ShopifyRestApiCredentials(user.ShopDomain, user.AccessToken);
            
            // The service created by the factory automatically uses the injected RequestExecutionPolicy
            var orderService = orderServiceFactory.Create(credentials);
            
            var orders = await orderService.ListAsync();
        }
    }
  10. Register ShopifySharp service factories with AddShopifySharpServiceFactories()

    master

    The services.AddShopifySharpServiceFactories() method adds all of ShopifySharp's service factory classes to your IServiceCollection as singletons.

    Note: This method does not add a request execution policy. However, the factories will automatically use any IRequestExecutionPolicy that you have added to the DI container via AddShopifySharpRequestExecutionPolicy<T>().

    services.AddShopifySharpServiceFactories();
  11. Parse and write GraphQL schema using Parser.ParseAndWriteAsync

    master

    The Parser.ParseAndWriteAsync method is the primary entrypoint for the parser. It accepts a raw GraphQL schema document and executes the full pipeline: parsing the schema, performing reachability analysis to filter dead code, generating C# models and fluent query builders, and finally writing the resulting code to disk using Roslyn-based splitting.

    // Note: Exact signature depends on the implementation of Parser.ParseAndWriteAsync
    // but it is the primary entrypoint for the pipeline.
    await Parser.ParseAndWriteAsync(rawGraphQLSchemaDocument);