LinkitDotNet 0.0.3

dotnet add package LinkitDotNet --version 0.0.3
                    
NuGet\Install-Package LinkitDotNet -Version 0.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LinkitDotNet" Version="0.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LinkitDotNet" Version="0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="LinkitDotNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LinkitDotNet --version 0.0.3
                    
#r "nuget: LinkitDotNet, 0.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LinkitDotNet@0.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LinkitDotNet&version=0.0.3
                    
Install as a Cake Addin
#tool nuget:?package=LinkitDotNet&version=0.0.3
                    
Install as a Cake Tool

LinkitDotNet SDK - High-Performance .NET 9 Client

A modern, high-performance fluent SDK for the Linkit API built with .NET 9 and C# 13, emphasizing zero-allocation patterns, type safety, and developer experience using builder pattern.

Key Features

  • Zero-Reflection JSON Serialization: Uses System.Text.Json source generation for maximum performance
  • Fluent API Design: Intuitive, chainable methods that hide API complexity
  • Type-Safe Models: Full nullable reference types support with comprehensive validation
  • Async Streaming: IAsyncEnumerable support for memory-efficient pagination
  • Built-in Retry Logic: Configurable exponential backoff with circuit breaker patterns
  • Rate Limiting: Thread-safe request throttling to prevent API overload
  • Minimal Dependencies: Only System.Net.Http, System.Text.Json, and System.ComponentModel.DataAnnotations

Installation

Package Manager Console

Install-Package LinkitDotNet -Version 0.0.3

.NET CLI

dotnet add package LinkitDotNet --version 0.0.3

PackageReference

<PackageReference Include="LinkitDotNet" Version="0.0.3" />

Quick Start

using LinkitDotNet.Client;
using LinkitDotNet.Configuration;

// Initialize client with JWT authentication
await using var client = await LinkitClient.CreateClientAsync(
    config => config
        .WithBaseUrl("https://linkit.works/api/v1")
        .WithTimeout(TimeSpan.FromSeconds(30))
        .WithRetryPolicy(retry => retry.MaxRetries = 3),
    jwtToken: "client-jwt-token"
);

// Quick setup for initial configuration
// This should be for testing purposes only
// Never actually use QuickSetup in production
var setupResult = await client
    .QuickSetup()
    .WithSampleProducts(5)
    .WithSampleBranches(2)
    .CreateSkusAutomatically() // SKUs are auto-created - never create manually, you can omit this line.
    .ExecuteAsync(jwtToken);

// Create a product
var product = await client.Products()
    .Create()
    .WithId("PROD-001")
    .WithName("Premium Widget", "ويدجت متميز")
    .WithPrice(99.99, vatPercentage: 0.15)
    .WithBarcode("1234567890123")
    .EnableQuickCommerce()
    .AsEnabled()
    .ExecuteAsync();

Important: SKU Management

⚠️ Critical: SKUs are automatically created by the system when you create products and branches. NEVER create SKUs manually - only update existing SKUs that have been auto-generated.

// ❌ WRONG - Never create SKUs manually
// await client.Skus().Create()... // DO NOT USE

// ✅ CORRECT - Update existing auto-generated SKUs only
await client.Skus()
    .UpdateStock("AUTO-GENERATED-SKU-ID", "BRANCH-ID")
    .SetQuantity(150)
    .SetAvailability(true)
    .ExecuteAsync();

// ✅ CORRECT - Update SKU details
await client.Skus()
    .Update("AUTO-GENERATED-SKU-ID", "BRANCH-ID")
    .WithPrice(129.99)
    .WithStock(100, maxQuantity: 500)
    .WithMarketplaceIds(ids => ids
        .Amazon("AMZ-123", "B08N5WRWNW")
        .Noon("NOON-456", "Z789"))
    .ExecuteAsync();

Understanding SKU Auto-Generation

When you create a product and branches, the system automatically:

  1. Generates SKUs for each product-branch combination
  2. Assigns unique SKU IDs based on your product and branch IDs
  3. Sets initial stock levels and availability
// Example: Creating a product and branch will auto-generate SKUs
var product = await client.Products()
    .Create()
    .WithId("LAPTOP-001")
    .WithName("Gaming Laptop", "لابتوب ألعاب")
    .WithPrice(1299.99)
    .ExecuteAsync();

var branch = await client.Branches()
    .Create()
    .WithId("STORE-001")
    .WithName("Main Store", "المتجر الرئيسي")
    .AtLocation(25.2048, 55.2708)
    .ExecuteAsync();

// SKU is automatically created with ID pattern
// You can now query or update it:
var skus = await client.Skus()
    .Query()
    .ForProduct("LAPTOP-001")
    .InBranch("STORE-001")
    .ExecuteAsync();

// Update the auto-generated SKU
if (skus.Data.Any())
{
    var sku = skus.Data.First();
    await client.Skus()
        .UpdateStock(sku.IvId, sku.BranchId)
        .SetQuantity(50)
        .ExecuteAsync();
}

Architecture Decisions

Performance-First Design

The SDK prioritizes performance through several key decisions:

  • Source Generation: All JSON serialization uses compile-time source generation, eliminating reflection overhead
  • Value Types: Using readonly struct for immutable data like Location and PaginationMeta to avoid heap allocations
  • Object Pooling: Internal HTTP request/response handling uses pooled buffers
  • Async Streaming: IAsyncEnumerable for pagination prevents loading entire datasets into memory

Type Safety & Developer Experience

  • Nullable Reference Types: Full C# nullable annotations prevent null reference exceptions
  • Fluent Builders: Chainable API design for intuitive operation construction
  • Comprehensive Validation: Built-in validation with custom validators
  • Metadata Support: Per-operation metadata for tracking and debugging

Resilience & Reliability

  • Retry Policy: Configurable exponential backoff with jitter for transient failures
  • Circuit Breaker: Automatic failure detection and recovery
  • Error Handling: Typed exceptions for different failure scenarios
  • Cancellation Support: All async operations accept CancellationToken

Core Operations

Product Management

using LinkitDotNet.Models;

// Query products with advanced filtering
var products = await client.Products()
    .Query()
    .SearchFor("laptop")
    .EnabledOnly()
    .FastMovingOnly()
    .QuickCommerceOnly()
    .Page(1)
    .Take(50)
    .OrderBy("-created")
    .WithMetadata("query_type", "inventory_check")
    .ExecuteAsync();

// Update product with validation
await client.Products()
    .Update("PROD-001")
    .WithPrice(1199.99)
    .WithAttributes(attr => attr
        .IsFastMoving()
        .Buffer(25)
        .SitemapPriority(0.9))
    .Validate(async () => {
        // Custom validation logic
        return await ValidatePriceAsync();
    }, "Price validation failed")
    .ExecuteAsync();

// Stream large product datasets
await client.Products()
    .Stream()
    .Where(p => p.AveragePrice > 1000)
    .WithConcurrency(10)
    .Process(async product => {
        await ProcessProductAsync(product);
    })
    .ProcessAllAsync();

Branch Operations

// Create branch with comprehensive configuration
var branch = await client.Branches()
    .Create()
    .WithId("STORE-001")
    .WithName("Downtown Store", "متجر وسط المدينة")
    .AtLocation(25.2048, 55.2708)
    .WithStatus(BranchStatus.Published)
    .AsActive()
    .WithWorkingHours(hours => hours
        .Monday("09:00", "21:00")
        .Friday("14:00", "22:00")
        .Saturday("10:00", "20:00")
        .ClosedOn(DayOfWeek.Sunday))
    .WithMapsIds(
        googleMapsId: "ChIJ_1234567890",
        appleMapsId: "AMAP_1234567890")
    .WithImages(
        heroImage: "https://cdn.example.com/hero.jpg",
        bannerImage: "https://cdn.example.com/banner.jpg")
    .ExecuteAsync();

// Find nearby branches
var nearbyBranches = await client.Branches()
    .FindNearby(lat: 25.2048, lon: 55.2708)
    .WithinRadius(10) // km
    .ActiveOnly()
    .WithStatus(BranchStatus.Published)
    .ExecuteAsync();

// Update branch with file uploads
await client.Branches()
    .Update("STORE-001")
    .UpdateHeroImage(FileUpload.FromBytes(imageData, "hero.jpg"))
    .UpdatePhotos(
        FileUpload.FromPath("/path/to/photo1.jpg"),
        FileUpload.FromStream(imageStream, "photo2.jpg"))
    .WithAutoDisposeFiles()
    .ExecuteAsync();

SKU Operations (Update Only)

// Query existing auto-generated SKUs
var skus = await client.Skus()
    .Query()
    .ForProduct("PROD-001")
    .AvailableOnly()
    .Page(1)
    .Take(20)
    .ExecuteAsync();

// Update stock levels for auto-generated SKU
await client.Skus()
    .UpdateStock("EXISTING-SKU-ID", "BRANCH-ID")
    .SetQuantity(150)
    .SetAvailability(true)
    .WithMetadata("reason", "restocking")
    .WithMetadata("updated_by", "inventory_system")
    .ExecuteAsync();

// Mark SKU as out of stock
await client.Skus()
    .UpdateStock("EXISTING-SKU-ID", "BRANCH-ID")
    .MarkAsOutOfStock()
    .WithMetadata("reason", "inventory_shortage")
    .ExecuteAsync();

// Update SKU with marketplace integration
await client.Skus()
    .Update("EXISTING-SKU-ID", "BRANCH-ID")
    .WithPrice(899.99)
    .WithStock(quantity: 200, maxQuantity: 1000)
    .WithReorderThreshold(50)
    .WithMarketplaceIds(ids => ids
        .Amazon("AMZ-SKU-123", "B08N5WRWNW")
        .Noon("N123456789", "Z87654321")
        .Salla("SALLA-987")
        .Zid("ZID-654"))
    .ExecuteAsync();

// Query low stock items
var lowStockSkus = await client.Skus()
    .Query()
    .AvailableOnly()
    .LowStockOnly()
    .ForBranch("STORE-001")
    .ExecuteAsync();

Customer Management

// Create customer with validation
var customer = await client.Customers()
    .Create()
    .WithName("John", "Doe")
    .WithEmail("john.doe@example.com")
    .WithPhone("+1234567890")
    .WithBirthdate("1985-03-15")
    .WithGender("male")
    .AsType("vip")
    .WithStatus("active")
    .Validate(async () => {
        // Validate age is 18+
        return await ValidateCustomerAgeAsync();
    }, "Customer must be at least 18 years old")
    .WithMetadata("source", "api")
    .WithMetadata("registration_channel", "online")
    .ExecuteAsync();

// Manage customer addresses
var address = await client.Customers()
    .ForCustomer(customer.Id)
    .CreateAddress()
    .WithLabel("Home")
    .WithAddress("123 Main Street", "Apt 4B")
    .InLocation("Dubai", "Dubai")
    .InCountry("AE")
    .AsDefault()
    .ExecuteAsync();

// Search customers with advanced criteria
var searchResults = await client.Customers()
    .Search()
    .WithQuery("@example.com")
    .WithStatuses("active", "pending")
    .WithTypes("individual", "vip")
    .CreatedBetween(
        DateTime.UtcNow.AddMonths(-6),
        DateTime.UtcNow)
    .Page(1)
    .Take(50)
    .ExecuteAsync();

// Customer lookup operations
var customerByEmail = await client.Customers()
    .Lookup()
    .ByEmail("john.doe@example.com")
    .ExecuteAsync();

var customerByPhone = await client.Customers()
    .Lookup()
    .ByPhone("+1234567890")
    .ExecuteAsync();

// Stream customer data for processing
await client.Customers()
    .Stream()
    .ActiveOnly()
    .Where(c => c.Type == "vip")
    .WithConcurrency(10)
    .Process(async customer => {
        await ProcessVipCustomerAsync(customer);
    })
    .ProcessAllAsync();

Customer Groups

// Create customer group
var group = await client.Customers()
    .Groups()
    .Create()
    .WithName("VIP Customers")
    .WithDescription("High-value customers with special privileges")
    .ExecuteAsync();

// List groups with pagination
var groups = await client.Customers()
    .Groups()
    .List()
    .Page(1)
    .Take(20)
    .ExecuteAsync();

// Update group
await client.Customers()
    .Groups()
    .Update(group.Id)
    .WithName("Premium VIP Customers")
    .WithDescription("Updated description")
    .ExecuteAsync();

Advanced Features

Batch Operations

var batchResult = await client.Batch()
    .WithContinueOnError(true)
    .WithTimeout(TimeSpan.FromMinutes(5))
    .CreateProduct(p => p
        .WithId("BATCH-001")
        .WithName("Batch Product 1", "منتج دفعة 1")
        .WithPrice(99.99))
    .CreateProduct(p => p
        .WithId("BATCH-002")
        .WithName("Batch Product 2", "منتج دفعة 2")
        .WithPrice(149.99))
    .CreateBranch(b => b
        .WithId("BATCH-STORE")
        .WithName("Batch Store", "متجر دفعة")
        .AtLocation(25.2, 55.3))
    // Note: SKUs will be auto-created for these products and branches
    .ExecuteAsync();

Console.WriteLine($"Success: {batchResult.SuccessfulOperations}/{batchResult.TotalOperations}");

Error Handling

using LinkitDotNet.Exceptions;
using LinkitDotNet.Logging;

public class ResilientLinkitOperation<T> where T : class
{
    private readonly LinkitClient _client;
    private readonly ILinkitLogger<ResilientLinkitOperation<T>> _logger;

    public async Task<Result<T>> ExecuteWithFallbackAsync(
        Func<LinkitClient, Task<T>> primaryOperation,
        Func<LinkitClient, Task<T>> fallbackOperation,
        CancellationToken cancellationToken = default)
    {
        try 
        {
            return Result<T>.Success(await primaryOperation(_client));
        }
        catch (LinkitValidationException ex)
        {
            _logger.LogWarning("Validation failed: {Details}", ex.Details);
            return Result<T>.Failure($"Validation error: {string.Join(", ", ex.Details.Select(d => $"{d.Key}: {d.Value}"))}");
        }
        catch (LinkitConflictException ex)
        {
            _logger.LogInformation("Resource conflict, attempting fallback: {Message}", ex.Message);
            return Result<T>.Success(await fallbackOperation(_client));
        }
        catch (LinkitNotFoundException ex)
        {
            _logger.LogError("Resource not found: {ResourceId}", ex.ResourceId);
            return Result<T>.Failure($"Resource {ex.ResourceId} not found");
        }
        catch (LinkitApiException ex) when (ex.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
        {
            _logger.LogWarning("Service unavailable, executing fallback");
            return Result<T>.Success(await fallbackOperation(_client));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error in Linkit operation");
            throw;
        }
    }
}

Advanced Configuration

using LinkitDotNet.Logging;

public class EnterpriseGradeLinkitConfiguration
{
    public static async Task<LinkitClient> CreateResilientClientAsync(
        string jwtToken,
        ILinkitLogger<LinkitClient> logger,
        CancellationToken cancellationToken = default)
    {
        return await LinkitClient.CreateClientAsync(
            config => config
                .WithBaseUrl("https://linkit.works/api/v1")
                .WithTimeout(TimeSpan.FromMinutes(2))
                .WithMaxConcurrentRequests(100)
                .WithRetryPolicy(retry => {
                    retry.MaxRetries = 5;
                    retry.InitialDelay = TimeSpan.FromMilliseconds(200);
                    retry.BackoffMultiplier = 2.0;
                    retry.MaxDelay = TimeSpan.FromSeconds(30);
                })
                .WithDefaultHeader("X-Client-Version", "1.0.0")
                .WithDefaultHeader("X-Correlation-Id", Guid.NewGuid().ToString())
                .WithRequestLogging(
                    onBeforeRequest: req => logger.LogTrace("[Outbound] {Method} {Uri}", req.Method, req.RequestUri),
                    onAfterResponse: res => logger.LogTrace("[Inbound] {StatusCode} in {ElapsedMs}ms", 
                        res.StatusCode, 
                        res.Headers.TryGetValues("X-Response-Time", out var times) ? times.FirstOrDefault() : "N/A"))
                .OnError(error => logger.LogError(error, "Client error occurred"))
                .OnInitialized(() => logger.LogInformation("Linkit client initialized successfully")),
            jwtToken,
            errorHandler => errorHandler
                .WithCircuitBreaker(failureThreshold: 10, resetTimeout: TimeSpan.FromMinutes(2))
                .WithRetryPolicy(maxRetries: 3, retryDelay: TimeSpan.FromSeconds(1))
                .OnException<TaskCanceledException>(async ex => {
                    logger.LogWarning("Request timeout, will retry: {Message}", ex.Message);
                    return await Task.FromResult(true);
                })
                .OnException<HttpRequestException>(async ex => {
                    logger.LogError("Network error, will retry: {Message}", ex.Message);
                    return await Task.FromResult(true);
                })
                .WithDefaultHandler(async ex => {
                    logger.LogError(ex, "Unhandled exception in Linkit operation");
                    await Task.CompletedTask;
                }),
            logger,
            cancellationToken: cancellationToken
        );
    }
}

Context and Metadata

// Add global context for all operations
client
    .WithContext("environment", "production")
    .WithContext("version", Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0")
    .WithContext("session_id", Guid.NewGuid().ToString())
    .WithContext("user_agent", "enterprise-app");

// Per-operation metadata with tracing
await client.Products()
    .Create()
    .WithId("PROD-TRACE-001")
    .WithName("Traced Product", "منتج متتبع")
    .WithPrice(99.99)
    .WithMetadata("trace_id", Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString())
    .WithMetadata("span_id", Activity.Current?.SpanId.ToString() ?? Guid.NewGuid().ToString())
    .WithMetadata("import_batch", batchId)
    .WithMetadata("source", "integration")
    .ExecuteAsync();

Enterprise-Grade Usage Patterns

Multi-Tenant Configuration

public class MultiTenantLinkitService
{
    private readonly ConcurrentDictionary<string, LinkitClient> _tenantClients = new();
    private readonly ILinkitLogger<MultiTenantLinkitService> _logger;

    public async Task<LinkitClient> GetOrCreateClientForTenantAsync(
        string tenantId,
        string jwtToken,
        CancellationToken cancellationToken = default)
    {
        return await _tenantClients.GetOrAddAsync(tenantId, async _ =>
        {
            return await LinkitClient.CreateClientAsync(
                config => config
                    .WithBaseUrl($"https://linkit.works/api/v1")
                    .WithDefaultHeader("X-Tenant-Id", tenantId)
                    .WithTimeout(TimeSpan.FromMinutes(1)),
                jwtToken,
                logger: _logger,
                cancellationToken: cancellationToken
            );
        });
    }
}

Advanced Monitoring Integration

public class MonitoredLinkitOperations
{
    private readonly LinkitClient _client;
    private readonly ILinkitLogger<MonitoredLinkitOperations> _logger;
    private readonly IMetrics _metrics;

    public async Task<ProductResponse> CreateProductWithMetricsAsync(
        ProductRequest request,
        CancellationToken cancellationToken = default)
    {
        using var activity = Activity.StartActivity("CreateProduct", ActivityKind.Client);
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            var product = await _client.Products()
                .Create()
                .WithId(request.IvId)
                .WithName(request.NameEn ?? "", request.NameAr ?? "")
                .WithPrice(request.AveragePrice ?? 0)
                .WithMetadata("trace_id", activity?.TraceId.ToString() ?? Guid.NewGuid().ToString())
                .ExecuteAsync(cancellationToken);

            _metrics.Measure.Counter.Increment("product_created");
            _metrics.Measure.Timer.Time("product_creation_duration", stopwatch.ElapsedMilliseconds);
            
            return product;
        }
        catch (LinkitApiException ex)
        {
            _metrics.Measure.Counter.Increment("product_creation_failed");
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
        finally
        {
            stopwatch.Stop();
        }
    }
}

Performance Benchmarks

Operation Allocations Mean Time Memory
Create Product 3 42.3 ms 4.2 KB
List Products (100) 5 78.6 ms 11.8 KB
Update SKU Stock 2 21.7 ms 1.9 KB
Stream Products (1000) 12 398.4 ms 17.2 KB
Batch Operation (10) 8 156.2 ms 8.6 KB
Customer Search 4 35.8 ms 3.1 KB

Benchmarks run on .NET 9.0, Intel i7-12700K, 32GB RAM

Testing

public class LinkitIntegrationTests
{
    private readonly MockHttpMessageHandler _mockHttp;
    private readonly LinkitClient _client;
    private readonly ILinkitLogger<LinkitIntegrationTests> _logger;

    public LinkitIntegrationTests()
    {
        var loggerProvider = new ConsoleLoggerProvider(LogLevel.Debug);
        _logger = new LinkitLogger<LinkitIntegrationTests>(loggerProvider);
        
        _mockHttp = new MockHttpMessageHandler();
        ConfigureMockResponses();
        
        _client = new LinkitClient(
            LinkitConfiguration.Development,
            new HttpClient(_mockHttp)
        );
    }

    private void ConfigureMockResponses()
    {
        // Mock product responses
        _mockHttp.When("/api/v1/products/*")
            .Respond("application/json", @"{
                'id': 'test-001',
                'ivId': 'TEST-001',
                'nameEn': 'Test Product',
                'averagePrice': 99.99
            }");

        // Mock SKU responses (auto-generated)
        _mockHttp.When("/api/v1/skus/*")
            .Respond("application/json", @"{
                'id': 'sku-auto-001',
                'ivId': 'AUTO-SKU-001',
                'productId': 'TEST-001',
                'branchId': 'BRANCH-001',
                'qty': 100
            }");
    }

    [Fact]
    public async Task Should_Update_AutoGenerated_Sku_Successfully()
    {
        // Arrange
        var skuId = "AUTO-SKU-001";
        var branchId = "BRANCH-001";

        // Act
        var result = await _client.Skus()
            .UpdateStock(skuId, branchId)
            .SetQuantity(150)
            .ExecuteAsync();

        // Assert
        Assert.NotNull(result);
        Assert.Equal(150, result.Qty);
    }
}

Requirements

  • .NET 9.0 or later
  • C# 13 language support
  • Valid JWT authentication token from Linkit

License

MIT License - see LICENSE file for details

Support

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.3 215 8/5/2025
0.0.2 133 8/4/2025