# .NET SDK

The Apiera .NET SDK provides a type-safe client for the Apiera API with built-in authentication, token caching, and
rate limiting.

## Installation

The SDK is distributed as three NuGet packages:


```bash
dotnet add package Apiera.Dotnet.Sdk
dotnet add package Apiera.Dotnet.Sdk.Auth
dotnet add package Apiera.Dotnet.Sdk.Rest
```

| Package | Purpose |
|  --- | --- |
| `Apiera.Dotnet.Sdk` | Client factory, dependency injection, and rate limiting. |
| `Apiera.Dotnet.Sdk.Auth` | Token provider with automatic caching. |
| `Apiera.Dotnet.Sdk.Rest` | Generated request builders and models. |


## Setup

### Dependency injection (recommended)

Register the SDK in your service collection with a single call:


```csharp
using Apiera.Dotnet.Sdk;
using Apiera.Dotnet.Sdk.Auth;

builder.Services.AddFusionCache();
builder.Services.AddApieraSdk(options =>
{
    options.BaseUrl = "https://api.apiera.io";
    options.Auth = new AuthConfiguration
    {
        Domain = "auth.apiera.io",
        ClientId = "your-client-id",
        ClientSecret = "your-client-secret",
        Audience = "https://api.apiera.io",
        Organization = "your-organization-id",
    };
});
```

Then inject `ApieraClient` into your services:


```csharp
public class ProductService(ApieraClient client)
{
    public async Task<IEnumerable<ProductResponse>> GetProductsAsync()
    {
        var result = await client.Api.V1.Products.GetAsync();
        return result?.Data ?? [];
    }
}
```

### Factory pattern (multi-tenant)

When you need to work with multiple organizations or credentials, use `IApieraClientFactory` directly:


```csharp
builder.Services.AddFusionCache();
builder.Services.AddApieraSdk();
```


```csharp
public class MultiTenantService(IApieraClientFactory factory)
{
    public async Task SyncProducts(ApieraClientOptions tenantOptions)
    {
        await using var client = factory.CreateClient(tenantOptions);
        var products = await client.Api.V1.Products.GetAsync();
        // Process products...
    }
}
```

`ApieraClient` implements `IAsyncDisposable`. When using the factory pattern, always dispose the client when you are
done to clean up the rate limiter.

## Configuration

### ApieraClientOptions

| Property | Type | Description |
|  --- | --- | --- |
| `BaseUrl` | `string` | **Required.** The API base URL (e.g. `https://api.apiera.io`). |
| `Auth` | `AuthConfiguration` | **Required.** Authentication credentials. |
| `RateLimit` | `RateLimitOptions` | Optional. Client-side rate limit settings. |


### AuthConfiguration

| Property | Type | Description |
|  --- | --- | --- |
| `Domain` | `string` | **Required.** The authentication domain. |
| `ClientId` | `string` | **Required.** Your client ID. |
| `ClientSecret` | `string` | **Required.** Your client secret. |
| `Audience` | `string` | **Required.** The API audience identifier. |
| `Organization` | `string` | **Required.** Your organization identifier. |


### RateLimitOptions

| Property | Type | Default | Description |
|  --- | --- | --- | --- |
| `BurstSize` | `int` | `10` | Maximum burst capacity. |
| `ReplenishmentMs` | `int` | `1000` | Token replenishment interval in milliseconds. |
| `TokensPerPeriod` | `int` | `10` | Tokens added per replenishment interval. |
| `QueueLimit` | `int` | `200` | Maximum queued requests before rejection. |


## Making requests

The SDK uses a fluent request builder pattern. Every resource in the API maps to a property chain starting from
`client.Api.V1`.

### List resources


```csharp
var result = await client.Api.V1.Products.GetAsync();

foreach (var product in result?.Data ?? [])
{
    Console.WriteLine($"{product.Uuid} - {product.DisplayName}");
}
```

### Get a single resource


```csharp
var productId = Guid.Parse("a1b2c3d4-...");
var product = await client.Api.V1.Products[productId].GetAsync();

Console.WriteLine(product?.DisplayName);
Console.WriteLine(product?.Status); // ProductResponse_status.Published
```

### Create a resource


```csharp
var product = await client.Api.V1.Products.PostAsync(new ProductCreateRequest
{
    Code = "PROD-001",
    DisplayName = "Example Product",
    Type = ProductCreateRequest_type.Simple,
});
```

### Update a resource


```csharp
var productId = Guid.Parse("a1b2c3d4-...");

await client.Api.V1.Products[productId].PutAsync(new ProductUpdateRequest
{
    Code = "PROD-001-UPDATED",
    DisplayName = "Updated Product",
});
```

### Delete a resource


```csharp
var productId = Guid.Parse("a1b2c3d4-...");
await client.Api.V1.Products[productId].DeleteAsync();
```

## Querying and filtering

List endpoints accept typed query parameters for filtering, pagination, and sorting:


```csharp
var result = await client.Api.V1.Products.GetAsync(config =>
{
    config.QueryParameters.Page = 1;
    config.QueryParameters.Size = 25;
    config.QueryParameters.DisplayNameContains = "shirt";
    config.QueryParameters.StatusEq = [
        GetStatusEqQueryParameterType.Published,
        GetStatusEqQueryParameterType.Draft,
    ];
});
```

### Common query parameters

| Parameter | Description |
|  --- | --- |
| `Page`, `Size` | Pagination. |
| `CodeContains`, `CodeStarts`, `CodeEnds` | Filter by code. |
| `DisplayNameContains` | Filter by display name. |
| `StatusEq` | Filter by status. |
| `CreatedAtMin`, `CreatedAtMax` | Filter by creation date. |
| `FamilyUuidsEq` | Filter by product family. |
| `BrandUuidsEq` | Filter by brand. |


## Hydration

Use `Include` to get arrays of related UUIDs, or `Expand` to get full related objects:


```csharp
var result = await client.Api.V1.Products.GetAsync(config =>
{
    // Include UUIDs of related resources
    config.QueryParameters.IncludeProductAttributes = true;
    config.QueryParameters.IncludeProductCategories = true;

    // Expand to get full related objects
    config.QueryParameters.ExpandProductAttributes = true;
});
```

On a single resource:


```csharp
var productId = Guid.Parse("a1b2c3d4-...");
var product = await client.Api.V1.Products[productId].GetAsync(config =>
{
    config.QueryParameters.ExpandProductAssets = true;
    config.QueryParameters.ExpandProductInputValues = true;
    config.QueryParameters.ExpandCompleteness = true;
});
```

## Lifecycle transitions

Invoke lifecycle transitions through the actions endpoint:


```csharp
var productId = Guid.Parse("a1b2c3d4-...");

// Publish a product
await client.Api.V1.Products[productId].Actions.Lifecycle.PatchAsync(
    new LifecycleTransitionRequest
    {
        Transition = LifecycleTransitionRequest_transition.Publish,
    });

// Unpublish a product
await client.Api.V1.Products[productId].Actions.Lifecycle.PatchAsync(
    new LifecycleTransitionRequest
    {
        Transition = LifecycleTransitionRequest_transition.Unpublish,
    });
```

## Sub-resources

Access sub-resources through the parent resource's request builder:


```csharp
var productId = Guid.Parse("a1b2c3d4-...");

// List product attributes
var attributes = await client.Api.V1.Products[productId].Attributes.GetAsync();

// Add a category to a product
await client.Api.V1.Products[productId].Categories.PostAsync(
    new ProductCategoryCreateRequest
    {
        CategoryUuid = Guid.Parse("f47ac10b-..."),
    });

// List product relations
var relations = await client.Api.V1.Products[productId].Relations.GetAsync();
```

## Error handling

The SDK throws `ApiException` for API errors. The response body contains a
[problem details](https://datatracker.ietf.org/doc/html/rfc9457) object:


```csharp
using Microsoft.Kiota.Abstractions;

try
{
    await client.Api.V1.Products.PostAsync(new ProductCreateRequest
    {
        Code = "PROD-001",
        DisplayName = "Example Product",
        Type = ProductCreateRequest_type.Simple,
    });
}
catch (ApiException ex)
{
    Console.WriteLine($"Status: {ex.ResponseStatusCode}");
    Console.WriteLine($"Message: {ex.Message}");
}
```

## Token caching

The SDK automatically caches access tokens using [FusionCache](https://github.com/ZiggyCreatures/FusionCache). Tokens
are cached until shortly before they expire, with a safety buffer to prevent using tokens that are about to become
invalid. No additional configuration is needed beyond registering FusionCache:


```csharp
builder.Services.AddFusionCache();
```

Token acquisition is an expensive operation. The SDK handles caching for you, but make sure you register FusionCache
in your DI container. Without it, every API call will request a new token.

## Rate limiting

The SDK includes a client-side token bucket rate limiter that prevents your application from exceeding the API's rate
limits. It is enabled by default with sensible defaults.

To customize the rate limiter:


```csharp
builder.Services.AddApieraSdk(options =>
{
    options.BaseUrl = "https://api.apiera.io";
    options.Auth = new AuthConfiguration { /* ... */ };
    options.RateLimit = new RateLimitOptions
    {
        BurstSize = 10,
        TokensPerPeriod = 10,
        ReplenishmentMs = 1000,
        QueueLimit = 200,
    };
});
```

When the rate limiter's queue is full, the SDK throws an `HttpRequestException`. Your application should handle this
gracefully with retry logic or backoff.

## Available resources

The SDK provides request builders for all API resources:

| Request builder | Endpoint |
|  --- | --- |
| `client.Api.V1.Products` | `/v1/products` |
| `client.Api.V1.ProductFamilies` | `/v1/product-families` |
| `client.Api.V1.Brands` | `/v1/brands` |
| `client.Api.V1.Categories` | `/v1/categories` |
| `client.Api.V1.Tags` | `/v1/tags` |
| `client.Api.V1.Attributes` | `/v1/attributes` |
| `client.Api.V1.Assets` | `/v1/assets` |
| `client.Api.V1.Channels` | `/v1/channels` |
| `client.Api.V1.Locales` | `/v1/locales` |
| `client.Api.V1.Collections` | `/v1/collections` |
| `client.Api.V1.InputTypes` | `/v1/input-types` |
| `client.Api.V1.TypeGroups` | `/v1/type-groups` |
| `client.Api.V1.WebhookSubscriptions` | `/v1/webhook-subscriptions` |