LinkitDotNet 0.0.3
dotnet add package LinkitDotNet --version 0.0.3
NuGet\Install-Package LinkitDotNet -Version 0.0.3
<PackageReference Include="LinkitDotNet" Version="0.0.3" />
<PackageVersion Include="LinkitDotNet" Version="0.0.3" />
<PackageReference Include="LinkitDotNet" />
paket add LinkitDotNet --version 0.0.3
#r "nuget: LinkitDotNet, 0.0.3"
#:package LinkitDotNet@0.0.3
#addin nuget:?package=LinkitDotNet&version=0.0.3
#tool nuget:?package=LinkitDotNet&version=0.0.3
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:
- Generates SKUs for each product-branch combination
- Assigns unique SKU IDs based on your product and branch IDs
- 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 likeLocation
andPaginationMeta
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
- API Documentation: https://linkit.works/docs
- SDK Issues: https://github.com/linkit/sdk-dotnet/issues
- Email: support@linkit.works
Product | Versions 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. |
-
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.