diff --git a/Docs/Rest-Parser-Usage.md b/Docs/Rest-Parser-Usage.md new file mode 100644 index 0000000..8a4eaf0 --- /dev/null +++ b/Docs/Rest-Parser-Usage.md @@ -0,0 +1,980 @@ +# REST-Parser Usage Guide + +This guide provides detailed information on how to use the REST-Parser library in your .NET applications. + +## Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Query Syntax Reference](#query-syntax-reference) +- [Operators](#operators) +- [Filtering Examples](#filtering-examples) +- [Sorting Examples](#sorting-examples) +- [Pagination Examples](#pagination-examples) +- [Advanced Usage](#advanced-usage) +- [Exception Handling](#exception-handling) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) + +--- + +## Installation + +### NuGet Package Manager +```bash +Install-Package REST-Parser +``` + +### .NET CLI +```bash +dotnet add package REST-Parser +``` + +### Package Reference +```xml + +``` + +--- + +## Quick Start + +### 1. Define Your Entity + +```csharp +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } + public string Category { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } + public DateTime ReleaseDate { get; set; } + public bool IsActive { get; set; } + public string? Description { get; set; } + public double? Rating { get; set; } +} +``` + +### 2. Register the Parser (Dependency Injection) + +```csharp +using REST_Parser.DependencyResolution; + +// In Program.cs or Startup.cs +builder.Services.RegisterRestParser(); +``` + +### 3. Inject and Use in Your Service/Controller + +```csharp +using REST_Parser; +using REST_Parser.Models; + +public class ProductService +{ + private readonly IRestToLinqParser _parser; + private readonly AppDbContext _context; + + public ProductService(IRestToLinqParser parser, AppDbContext context) + { + _parser = parser; + _context = context; + } + + public RestResult GetProducts(string query) + { + // Parse and execute the query + return _parser.Run(_context.Products, query); + } +} +``` + +### 4. Make a Request + +```http +GET /api/products?category=Electronics&price[lt]=1000&$sort_by=price[ASC]&$page=1&$pagesize=20 +``` + +--- + +## Query Syntax Reference + +### Basic Format + +``` +field[operator]=value +``` + +### Multiple Conditions + +Use `&` to separate conditions: + +``` +field1=value1&field2[operator]=value2&field3[operator]=value3 +``` + +### Special Parameters + +- **Sorting**: `$sort_by=field[ASC|DESC]` +- **Pagination**: `$page=n` and `$pagesize=n` + +### Whitespace Handling + +Whitespace is automatically trimmed: + +``` +field [eq] = value ✅ Valid +field[eq]=value ✅ Valid +field = value ✅ Valid (defaults to eq) +``` + +--- + +## Operators + +### Comparison Operators + +| Operator | Description | Supported Types | +|----------|-------------|-----------------| +| `eq` | Equal to (default) | All types | +| `ne` | Not equal to | All types | +| `gt` | Greater than | int, double, decimal, DateTime | +| `ge` | Greater than or equal | int, double, decimal, DateTime | +| `lt` | Less than | int, double, decimal, DateTime | +| `le` | Less than or equal | int, double, decimal, DateTime | + +### String Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `eq` | Exact match (case-sensitive) | `name[eq]=iPhone` | +| `ne` | Not equal | `name[ne]=Samsung` | +| `contains` | Contains substring (case-sensitive) | `name[contains]=Pro` | + +### Supported Data Types + +- ✅ `string` +- ✅ `int` / `int?` +- ✅ `double` / `double?` +- ✅ `decimal` / `decimal?` +- ✅ `DateTime` / `DateTime?` +- ✅ `bool` / `bool?` +- ✅ `Guid` / `Guid?` + +--- + +## Filtering Examples + +### String Filtering + +```csharp +// Exact match (default operator) +"name=iPhone" +"name[eq]=iPhone" + +// Not equal +"category[ne]=Electronics" + +// Contains (case-sensitive) +"description[contains]=wireless" + +// Multiple string conditions +"category=Electronics&brand=Apple" +``` + +**REST Query:** +```http +GET /api/products?name[contains]=Pro&category=Electronics +``` + +### Numeric Filtering + +```csharp +// Integer +"stock[gt]=10" // Stock greater than 10 +"stock[le]=100" // Stock less than or equal to 100 +"id=42" // ID equals 42 (default operator) + +// Decimal/Double +"price[lt]=999.99" // Price less than 999.99 +"rating[ge]=4.5" // Rating greater than or equal to 4.5 +``` + +**REST Query:** +```http +GET /api/products?price[gt]=100&price[lt]=1000&stock[gt]=0 +``` + +### Date Filtering + +```csharp +// Standard date formats +"releaseDate[gt]=2023-01-01" +"releaseDate[lt]=2024-12-31" + +// Date ranges +"releaseDate[ge]=2023-01-01&releaseDate[le]=2023-12-31" +``` + +**REST Query:** +```http +GET /api/products?releaseDate[gt]=2023-06-01&isActive=true +``` + +### Boolean Filtering + +```csharp +// Boolean values +"isActive=true" +"isActive[eq]=false" +"isDiscontinued[ne]=true" +``` + +**REST Query:** +```http +GET /api/products?isActive=true&isFeatured=true +``` + +### GUID Filtering + +```csharp +// GUID equality +"productId[eq]=123e4567-e89b-12d3-a456-426614174000" +"productId[ne]=123e4567-e89b-12d3-a456-426614174000" +``` + +### Nullable Field Filtering + +```csharp +// Works with nullable types +"rating[ge]=4.0" // double? +"discount[gt]=10" // decimal? +"lastPurchaseDate[lt]=2024-01-01" // DateTime? +``` + +--- + +## Sorting Examples + +### Single Column Sort + +```csharp +// Ascending (default) +"$sort_by=name" +"$sort_by=name[ASC]" + +// Descending +"$sort_by=price[DESC]" +``` + +**REST Query:** +```http +GET /api/products?$sort_by=price[DESC] +``` + +### Multiple Column Sort + +```csharp +// Sort by category ascending, then price descending +"$sort_by=category[ASC]&$sort_by=price[DESC]" + +// Sort by rating descending, then name ascending +"$sort_by=rating[DESC]&$sort_by=name[ASC]" +``` + +**REST Query:** +```http +GET /api/products?category=Electronics&$sort_by=brand[ASC]&$sort_by=price[ASC] +``` + +### Default Sort Behavior + +If no `$sort_by` is specified, results are automatically sorted by `Id` ascending: + +```csharp +// These are equivalent +"" +"$sort_by=Id[ASC]" +``` + +--- + +## Pagination Examples + +### Basic Pagination + +```csharp +// Get page 1 with 20 items +"$page=1&$pagesize=20" + +// Get page 2 with 50 items +"$page=2&$pagesize=50" +``` + +**REST Query:** +```http +GET /api/products?$page=2&$pagesize=25 +``` + +### Pagination with Filtering and Sorting + +```csharp +// Complex query with all features +"category=Electronics&price[lt]=1000&$sort_by=price[ASC]&$page=1&$pagesize=20" +``` + +**REST Query:** +```http +GET /api/products?category=Electronics&isActive=true&$sort_by=name[ASC]&$page=1&$pagesize=10 +``` + +### Default Pagination Behavior + +- **Default Page**: 1 (if `$page` is specified without value) +- **Default Page Size**: 25 (if `$pagesize` is specified without value) +- **Maximum Page Size**: 1000 (enforced by the parser) + +### Pagination Metadata + +The `RestResult` includes pagination information: + +```csharp +var result = _parser.Run(_context.Products, query); + +Console.WriteLine($"Page: {result.Page}"); +Console.WriteLine($"Page Size: {result.PageSize}"); +Console.WriteLine($"Total Count: {result.TotalCount}"); +Console.WriteLine($"Total Pages: {result.PageCount}"); + +// Access the data +var products = result.Data.ToList(); +``` + +--- + +## Advanced Usage + +### Parse vs Run + +#### Using `Parse()` - Just Parse, Don't Execute + +```csharp +// Parse the query without executing it +var parseResult = _parser.Parse("category=Electronics&price[lt]=1000"); + +// Inspect what was parsed +Console.WriteLine($"Number of filters: {parseResult.Expressions.Count}"); +Console.WriteLine($"Number of sorts: {parseResult.SortOrder.Count}"); +Console.WriteLine($"Page: {parseResult.Page}, PageSize: {parseResult.PageSize}"); + +// Apply manually with custom logic +IQueryable query = _context.Products; + +// Apply your own pre-filters +query = query.Where(p => p.IsActive); + +// Apply parsed expressions +foreach (var expression in parseResult.Expressions) +{ + query = query.Where(expression); +} + +// Apply sorting +var orderedQuery = query.OrderBy(parseResult.SortOrder[0].Expression); +// ... etc +``` + +#### Using `Run()` - Parse and Execute + +```csharp +// Parse and execute in one call +var result = _parser.Run(_context.Products, "category=Electronics&price[lt]=1000"); + +// Data is already filtered, sorted, and paginated +var products = result.Data.ToList(); +``` + +### Adding Custom Pre-Filters + +```csharp +// Parse the user's query +var result = _parser.Parse(userQuery); + +// Start with your base query +IQueryable query = _context.Products + .Where(p => p.IsActive) // Always filter active + .Where(p => p.TenantId == tenantId); // Tenant isolation + +// Apply user's filters +foreach (var expression in result.Expressions) +{ + query = query.Where(expression); +} + +// Continue with sorting and pagination... +``` + +### Using with Entity Framework Core + +```csharp +public async Task> GetProductsAsync(string query) +{ + var result = _parser.Run(_context.Products.AsNoTracking(), query); + + // Materialize the query + result.Data = result.Data.ToList().AsQueryable(); + + return result; +} +``` + +### Projection for Performance + +```csharp +var result = _parser.Run(_context.Products, query); + +// Project to DTOs to reduce data transfer +var data = result.Data + .Select(p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Price = p.Price, + Category = p.Category + }) + .ToList(); +``` + +### Multiple Entity Types + +```csharp +// Register multiple parsers +builder.Services.RegisterRestParser(); +builder.Services.RegisterRestParser(); +builder.Services.RegisterRestParser(); + +// Inject specific parsers +public class MultiService +{ + private readonly IRestToLinqParser _productParser; + private readonly IRestToLinqParser _customerParser; + + public MultiService( + IRestToLinqParser productParser, + IRestToLinqParser customerParser) + { + _productParser = productParser; + _customerParser = customerParser; + } +} +``` + +--- + +## Exception Handling + +### Exception Types + +The library throws three custom exceptions: + +```csharp +using REST_Parser.Exceptions; + +try +{ + var result = _parser.Run(_context.Products, query); + return Ok(result.Data.ToList()); +} +catch (REST_InvalidFieldnameException ex) +{ + // Field doesn't exist on the entity + // Example: "invalidField=value" + return BadRequest(new { error = "Invalid field", details = ex.Message }); +} +catch (REST_InvalidOperatorException ex) +{ + // Operator not supported for the field type + // Example: "name[gt]=test" (gt not valid for strings) + return BadRequest(new { error = "Invalid operator", details = ex.Message }); +} +catch (REST_InvalidValueException ex) +{ + // Value cannot be converted to the field's type + // Example: "price=notanumber" + return BadRequest(new { error = "Invalid value", details = ex.Message }); +} +catch (ArgumentException ex) +{ + // Security limits exceeded + // - Query too long (>2000 chars) + // - Too many conditions (>50) + // - Invalid condition format + return BadRequest(new { error = "Invalid query", details = ex.Message }); +} +``` + +### Validation in API Controller + +```csharp +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IRestToLinqParser _parser; + private readonly AppDbContext _context; + + public ProductsController(IRestToLinqParser parser, AppDbContext context) + { + _parser = parser; + _context = context; + } + + [HttpGet] + public IActionResult Get([FromQuery] string q) + { + // Provide default if empty + if (string.IsNullOrWhiteSpace(q)) + { + q = "$sort_by=Id&$page=1&$pagesize=20"; + } + + try + { + var result = _parser.Run(_context.Products, q); + + return Ok(new + { + data = result.Data.ToList(), + pagination = new + { + page = result.Page, + pageSize = result.PageSize, + totalCount = result.TotalCount, + totalPages = result.PageCount + } + }); + } + catch (REST_InvalidFieldnameException ex) + { + return BadRequest(new { error = "Invalid field name", message = ex.Message }); + } + catch (REST_InvalidOperatorException ex) + { + return BadRequest(new { error = "Invalid operator", message = ex.Message }); + } + catch (REST_InvalidValueException ex) + { + return BadRequest(new { error = "Invalid value", message = ex.Message }); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = "Invalid query format", message = ex.Message }); + } + catch (Exception ex) + { + // Log the exception + return StatusCode(500, new { error = "An error occurred processing your request" }); + } + } +} +``` + +--- + +## API Reference + +### IRestToLinqParser + +#### Methods + +**`RestResult Parse(string request)`** + +Parses a REST query string without executing it. + +- **Parameters**: + - `request` - The REST query string +- **Returns**: `RestResult` with parsed expressions and settings +- **Throws**: + - `ArgumentException` - Query exceeds limits + - `REST_InvalidFieldnameException` - Invalid field name + - `REST_InvalidOperatorException` - Invalid operator + - `REST_InvalidValueException` - Invalid value + +**`RestResult Run(IQueryable source, string rest)`** + +Parses and executes a REST query against a data source. + +- **Parameters**: + - `source` - The IQueryable data source + - `rest` - The REST query string +- **Returns**: `RestResult` with executed data and metadata +- **Throws**: Same as `Parse()` + +### RestResult + +#### Properties + +- **`List>> Expressions`** - Filter expressions +- **`List> SortOrder`** - Sort operations +- **`IQueryable Data`** - Query result (only populated by `Run()`) +- **`int Page`** - Current page number (1-based) +- **`int PageSize`** - Items per page +- **`int PageCount`** - Total number of pages +- **`int TotalCount`** - Total items matching filters + +### SortBy + +#### Properties + +- **`Expression> Expression`** - Sort expression +- **`bool Ascending`** - True for ascending, false for descending + +--- + +## Best Practices + +### 1. Always Validate Input + +```csharp +[HttpGet] +public IActionResult Get([FromQuery] string q = "") +{ + if (string.IsNullOrWhiteSpace(q)) + { + q = "$sort_by=Id&$pagesize=20"; // Sensible defaults + } + + // Use try-catch for exception handling + // ... +} +``` + +### 2. Enforce Maximum Page Size + +```csharp +var result = _parser.Run(_context.Products, query); + +// The parser already enforces MAX_PAGE_SIZE (1000) +// But you can add your own stricter limit +const int MAX_ALLOWED_PAGE_SIZE = 100; +if (result.PageSize > MAX_ALLOWED_PAGE_SIZE) +{ + return BadRequest($"Page size cannot exceed {MAX_ALLOWED_PAGE_SIZE}"); +} +``` + +### 3. Use DTOs for API Responses + +```csharp +var result = _parser.Run(_context.Products, query); + +var response = new +{ + data = result.Data.Select(p => new ProductDto + { + Id = p.Id, + Name = p.Name, + Price = p.Price + }).ToList(), + page = result.Page, + pageSize = result.PageSize, + totalCount = result.TotalCount, + totalPages = result.PageCount +}; + +return Ok(response); +``` + +### 4. Add Tenant/User Isolation + +```csharp +// Parse the query +var parsed = _parser.Parse(query); + +// Apply tenant filter first +IQueryable data = _context.Products + .Where(p => p.TenantId == currentTenantId); + +// Then apply user's filters +foreach (var expr in parsed.Expressions) +{ + data = data.Where(expr); +} +``` + +### 5. Use AsNoTracking for Read-Only Queries + +```csharp +var result = _parser.Run( + _context.Products.AsNoTracking(), + query +); +``` + +### 6. Log Failed Queries for Analysis + +```csharp +catch (REST_InvalidFieldnameException ex) +{ + _logger.LogWarning(ex, "Invalid field in query: {Query}", query); + return BadRequest(new { error = ex.Message }); +} +``` + +### 7. Cache Common Queries + +```csharp +// Use distributed cache for common queries +var cacheKey = $"products:{query}"; +var cached = await _cache.GetStringAsync(cacheKey); + +if (cached != null) +{ + return JsonSerializer.Deserialize(cached); +} + +var result = _parser.Run(_context.Products, query); +await _cache.SetStringAsync(cacheKey, + JsonSerializer.Serialize(result), + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }); +``` + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: "Query exceeds maximum length" + +**Cause**: Query string is longer than 2000 characters. + +**Solution**: Simplify your query or contact support if you need a larger limit. + +#### Issue: "Query exceeds maximum conditions" + +**Cause**: More than 50 filter conditions in the query. + +**Solution**: Reduce the number of conditions or use server-side filtering. + +#### Issue: "Invalid field name" + +**Cause**: Field doesn't exist on the entity. + +**Solution**: Check spelling and ensure the property exists: +```csharp +public class Product +{ + public int Id { get; set; } // Use: id=1 + public string Name { get; set; } // Use: name=iPhone +} +``` + +#### Issue: "Invalid operator" + +**Cause**: Using an operator not supported for that type. + +**Solution**: +- Strings: Only `eq`, `ne`, `contains` +- Numbers/Dates: `eq`, `ne`, `gt`, `ge`, `lt`, `le` +- Booleans: Only `eq`, `ne` + +#### Issue: "Invalid value" + +**Cause**: Value cannot be converted to the field's type. + +**Solution**: Ensure value matches the field type: +```csharp +price=999.99 // ✅ Correct for decimal +price=abc // ❌ Invalid +releaseDate=2023-01-01 // ✅ Correct for DateTime +releaseDate=notadate // ❌ Invalid +``` + +#### Issue: Case Sensitivity + +**Cause**: String comparisons are case-sensitive. + +**Solution**: +```csharp +name=iPhone // Matches "iPhone" but not "iphone" +name[contains]=pro // Matches "MacBook Pro" but not "MacBook PRO" +``` + +If you need case-insensitive search, handle it on the server: +```csharp +var result = _parser.Parse(query); +IQueryable data = _context.Products; + +// Apply case-insensitive filter manually +data = data.Where(p => p.Name.ToLower().Contains(searchTerm.ToLower())); + +// Then apply other filters +foreach (var expr in result.Expressions) +{ + data = data.Where(expr); +} +``` + +#### Issue: No Results Returned + +**Possible causes**: +1. Filters are too restrictive +2. Data doesn't exist +3. Tenant/user isolation filters + +**Debug**: +```csharp +var result = _parser.Parse(query); +Console.WriteLine($"Filters: {result.Expressions.Count}"); +Console.WriteLine($"Sort: {result.SortOrder.Count}"); + +// Test without filters +var allData = _context.Products.ToList(); +Console.WriteLine($"Total records: {allData.Count}"); +``` + +--- + +## Security Limits + +The parser enforces the following limits to prevent abuse: + +| Limit | Value | Description | +|-------|-------|-------------| +| MAX_QUERY_LENGTH | 2000 | Maximum query string length | +| MAX_CONDITIONS | 50 | Maximum number of filter conditions | +| MAX_PAGE_SIZE | 1000 | Maximum page size | + +These limits are enforced automatically and will throw `ArgumentException` if exceeded. + +--- + +## Complete Example + +Here's a complete working example: + +```csharp +// Entity +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } + public string Category { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } + public DateTime ReleaseDate { get; set; } + public bool IsActive { get; set; } +} + +// Startup/Program.cs +builder.Services.AddDbContext(); +builder.Services.RegisterRestParser(); + +// Controller +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IRestToLinqParser _parser; + private readonly AppDbContext _context; + private readonly ILogger _logger; + + public ProductsController( + IRestToLinqParser parser, + AppDbContext context, + ILogger logger) + { + _parser = parser; + _context = context; + _logger = logger; + } + + [HttpGet] + public IActionResult Get([FromQuery] string q = "") + { + try + { + // Default query if none provided + if (string.IsNullOrWhiteSpace(q)) + { + q = "$sort_by=Id&$pagesize=20"; + } + + // Parse and execute + var result = _parser.Run(_context.Products.AsNoTracking(), q); + + // Build response + return Ok(new + { + data = result.Data.Select(p => new + { + p.Id, + p.Name, + p.Category, + p.Price, + p.Stock + }).ToList(), + pagination = new + { + page = result.Page, + pageSize = result.PageSize, + totalCount = result.TotalCount, + totalPages = result.PageCount + } + }); + } + catch (REST_InvalidFieldnameException ex) + { + _logger.LogWarning(ex, "Invalid field in query: {Query}", q); + return BadRequest(new { error = "Invalid field name", message = ex.Message }); + } + catch (REST_InvalidOperatorException ex) + { + _logger.LogWarning(ex, "Invalid operator in query: {Query}", q); + return BadRequest(new { error = "Invalid operator", message = ex.Message }); + } + catch (REST_InvalidValueException ex) + { + _logger.LogWarning(ex, "Invalid value in query: {Query}", q); + return BadRequest(new { error = "Invalid value", message = ex.Message }); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, "Invalid query format: {Query}", q); + return BadRequest(new { error = "Invalid query", message = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing query: {Query}", q); + return StatusCode(500, new { error = "An error occurred processing your request" }); + } + } +} + +// Sample Requests +// GET /api/products +// GET /api/products?category=Electronics +// GET /api/products?price[gt]=100&price[lt]=1000 +// GET /api/products?name[contains]=Pro&isActive=true +// GET /api/products?category=Electronics&$sort_by=price[ASC]&$page=1&$pagesize=10 +``` + +--- + +## Additional Resources + +- **GitHub Repository**: https://github.com/BigBadJock/REST-Parser +- **NuGet Package**: https://www.nuget.org/packages/REST-Parser/ +- **Report Issues**: https://github.com/BigBadJock/REST-Parser/issues + +--- + +**Last Updated**: 2025 +**Library Version**: 1.2.5 +**Target Framework**: .NET 10 diff --git a/Docs/SilverCodeAPI-Usage-Guide.md b/Docs/SilverCodeAPI-Usage-Guide.md new file mode 100644 index 0000000..ec38834 --- /dev/null +++ b/Docs/SilverCodeAPI-Usage-Guide.md @@ -0,0 +1,1493 @@ +# SilverCodeAPI Usage Guide + +A .NET 10 library providing infrastructure for data access patterns including Repository, Unit of Work, and Data Service implementations with built-in REST query support. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Concepts](#core-concepts) +- [Repository Pattern](#repository-pattern) +- [Data Services](#data-services) +- [Database Factory & Unit of Work](#database-factory--unit-of-work) +- [Data Models](#data-models) +- [REST Query Integration](#rest-query-integration) +- [Auditing](#auditing) +- [Complete Examples](#complete-examples) +- [Best Practices](#best-practices) +- [Migration Guide](#migration-guide) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +SilverCodeAPI is a collection of NuGet packages that provide: + +- **Generic Repository Pattern** - CRUD operations with type-safe querying +- **Data Service Layer** - Business logic abstraction over repositories +- **REST Query Support** - Integrated with REST-Parser for flexible querying +- **Multiple ID Types** - Support for `int`, `Guid`, and `string` identifiers +- **Audit Tracking** - Built-in creation and modification tracking +- **Unit of Work** - Transaction management across repositories +- **Read-Only Repositories** - Separate interfaces for read and write operations + +### NuGet Packages + +| Package | Description | +|---------|-------------| +| `Core.Common.Contracts` | Interfaces for repositories, data services, and factories | +| `Core.Common.DataModels` | Base entity models and DTOs | +| `Core.Common` | Concrete implementations of repository and service patterns | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ Your API Controller │ +└──────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Data Service Layer │ +│ (BaseDataService, BaseDataServiceWithIntId, etc.) │ +└──────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Repository Layer │ +│ (BaseRepository, BaseRepositoryWithIntId, etc.) │ +└──────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Entity Framework Core DbContext │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## Installation + +### Package Manager Console + +```powershell +Install-Package Core.Common.Contracts +Install-Package Core.Common.DataModels +Install-Package Core.Common +Install-Package REST-Parser +``` + +### .NET CLI + +```bash +dotnet add package Core.Common.Contracts +dotnet add package Core.Common.DataModels +dotnet add package Core.Common +dotnet add package REST-Parser +``` + +### Package References + +```xml + + + + + + +``` + +--- + +## Quick Start + +### 1. Define Your Entity + +Choose a base model based on your ID type: + +```csharp +using Core.Common.DataModels; + +// Using integer ID +public class Product : BaseModelWithIntId +{ + public string Name { get; set; } + public string Category { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } +} + +// Using GUID ID +public class Order : BaseModelWithGuidId +{ + public string OrderNumber { get; set; } + public decimal Total { get; set; } + public DateTime OrderDate { get; set; } +} + +// Using string ID +public class UserProfile : BaseModelWithStringId +{ + public string Username { get; set; } + public string Email { get; set; } +} +``` + +### 2. Create Your DbContext + +```csharp +using Microsoft.EntityFrameworkCore; + +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Products { get; set; } + public DbSet Orders { get; set; } + public DbSet UserProfiles { get; set; } +} +``` + +### 3. Implement Repository + +```csharp +using Core.Common; +using Core.Common.Contracts; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using REST_Parser; + +public interface IProductRepository : IRepositoryWithIntId +{ + // Add custom methods if needed +} + +public class ProductRepository : BaseRepositoryWithIntId, IProductRepository +{ + public ProductRepository( + IDbContextFactory dbContextFactory, + IRestToLinqParser parser, + ILogger> logger) + : base(dbContextFactory, parser, logger) + { + } + + // Implement custom methods here +} +``` + +### 4. Implement Data Service + +```csharp +using Core.Common; +using Core.Common.Contracts; +using Microsoft.Extensions.Logging; + +public interface IProductService : IDataServiceWithIntId +{ + // Add custom business logic methods + Task GetProductByName(string name); +} + +public class ProductService : BaseDataServiceWithIntId, IProductService +{ + public ProductService( + IRepositoryWithIntId repository, + ILogger> logger) + : base(repository, logger) + { + } + + public async Task GetProductByName(string name) + { + return await repository.GetById(p => p.Name == name); + } +} +``` + +### 5. Register Services + +```csharp +using Microsoft.EntityFrameworkCore; +using REST_Parser.DependencyResolution; + +var builder = WebApplication.CreateBuilder(args); + +// Database +builder.Services.AddDbContextFactory(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// REST Parser +builder.Services.RegisterRestParser(); +builder.Services.RegisterRestParser(); +builder.Services.RegisterRestParser(); + +// Repositories +builder.Services.AddScoped(); + +// Data Services +builder.Services.AddScoped(); + +var app = builder.Build(); +``` + +### 6. Use in Controller + +```csharp +using Core.Common.DataModels; +using Microsoft.AspNetCore.Mvc; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IProductService _productService; + + public ProductsController(IProductService productService) + { + _productService = productService; + } + + // GET: api/products?category=Electronics&price[lt]=1000&$sort_by=price[ASC]&$page=1&$pagesize=20 + [HttpGet] + public IActionResult Search([FromQuery] string q = "") + { + var result = _productService.Search(q ?? "$sort_by=Id&$pagesize=20"); + + return Ok(new + { + data = result.Data, + pagination = result.Pagination + }); + } + + // GET: api/products/5 + [HttpGet("{id}")] + public async Task Get(int id) + { + var product = await _productService.GetById(id); + return product != null ? Ok(product) : NotFound(); + } + + // POST: api/products + [HttpPost] + public async Task Create([FromBody] Product product) + { + var created = await _productService.Add(product); + return CreatedAtAction(nameof(Get), new { id = created.Id }, created); + } + + // PUT: api/products/5 + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Product product) + { + if (id != product.Id) + return BadRequest(); + + var updated = await _productService.Update(product); + return Ok(updated); + } + + // DELETE: api/products/5 + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _productService.Delete(p => p.Id == id); + return success ? NoContent() : NotFound(); + } +} +``` + +--- + +## Core Concepts + +### ID Type Variants + +SilverCodeAPI provides three sets of base classes for different ID types: + +#### Integer IDs + +```csharp +// Base Model +public class Product : BaseModelWithIntId { } + +// Repository Interface +public interface IProductRepository : IRepositoryWithIntId { } + +// Repository Implementation +public class ProductRepository : BaseRepositoryWithIntId { } + +// Data Service Interface +public interface IProductService : IDataServiceWithIntId { } + +// Data Service Implementation +public class ProductService : BaseDataServiceWithIntId { } +``` + +#### GUID IDs + +```csharp +// Base Model +public class Order : BaseModelWithGuidId { } + +// Repository Interface +public interface IOrderRepository : IRepositoryWithGuidId { } + +// Repository Implementation +public class OrderRepository : BaseRepositoryWithGuidId { } + +// Data Service Interface +public interface IOrderService : IDataServiceWithGuidId { } + +// Data Service Implementation +public class OrderService : BaseDataServiceWithGuidId { } +``` + +#### String IDs + +```csharp +// Base Model +public class UserProfile : BaseModelWithStringId { } + +// Repository Interface +public interface IUserProfileRepository : IRepositoryWithStringId { } + +// Repository Implementation +public class UserProfileRepository : BaseRepositoryWithStringId { } + +// Data Service Interface +public interface IUserProfileService : IDataServiceWithStringId { } + +// Data Service Implementation +public class UserProfileService : BaseDataServiceWithStringId { } +``` + +### Read-Only Operations + +Separate read-only interfaces for query-only scenarios: + +```csharp +// Read-only repository +public interface IProductReadOnlyRepository : IReadRepositoryWithIntId { } + +public class ProductReadOnlyRepository : BaseReadRepositoryWithIntId, IProductReadOnlyRepository +{ + public ProductReadOnlyRepository( + IDbContextFactory dbContextFactory, + IRestToLinqParser parser, + ILogger> logger) + : base(dbContextFactory, parser, logger) + { + } +} +``` + +--- + +## Repository Pattern + +### Available Methods + +#### Read Operations + +```csharp +// Get all records +IQueryable products = repository.GetAll(); + +// Get with REST query +ApiResult result = repository.GetAll("category=Electronics&price[lt]=1000"); + +// Get by ID +Product product = await repository.GetById(5); + +// Get by ID with child entities +Product productWithChildren = await repository.GetById(5, includeChildren: true); + +// Get by condition +Product product = await repository.GetById(p => p.Name == "iPhone"); + +// Get multiple by condition +IEnumerable products = repository.GetMany(p => p.Category == "Electronics"); +``` + +#### Write Operations + +```csharp +// Add entity +Product newProduct = await repository.Add(product); + +// Add without immediate commit +Product newProduct = await repository.Add(product, commit: false); +await repository.Commit(); // Commit later + +// Batch add with progress reporting +IProgress progress = new Progress(report => +{ + Console.WriteLine($"{report.Message}: {report.CurrentProgress}/{report.TotalProgress}"); +}); + +await repository.AddBatch(products, batchSize: 100, progress); + +// Update entity +Product updated = await repository.Update(product); + +// Delete by condition +bool deleted = await repository.Delete(p => p.Id == 5); + +// Delete entity +bool deleted = await repository.Delete(product); + +// Manual commit +await repository.Commit(); +``` + +### Custom Repository Methods + +```csharp +public interface IProductRepository : IRepositoryWithIntId +{ + Task> GetLowStockProducts(int threshold); + Task> GetProductsByCategory(string category); + Task GetAveragePriceByCategory(string category); +} + +public class ProductRepository : BaseRepositoryWithIntId, IProductRepository +{ + public ProductRepository( + IDbContextFactory dbContextFactory, + IRestToLinqParser parser, + ILogger> logger) + : base(dbContextFactory, parser, logger) + { + } + + public async Task> GetLowStockProducts(int threshold) + { + return await Task.FromResult( + dbset.Where(p => p.Stock < threshold && !p.IsDeleted) + ); + } + + public async Task> GetProductsByCategory(string category) + { + return await Task.FromResult( + dbset.Where(p => p.Category == category && !p.IsDeleted) + ); + } + + public async Task GetAveragePriceByCategory(string category) + { + return await dbset + .Where(p => p.Category == category && !p.IsDeleted) + .AverageAsync(p => p.Price); + } +} +``` + +--- + +## Data Services + +### Available Methods + +```csharp +public interface IDataService +{ + Task Add(T model); + Task Update(T model); + Task Delete(Expression> where); + ApiResult Search(string restQuery); +} + +// With ID-specific methods +public interface IDataServiceWithIntId +{ + // All IDataService methods plus: + Task GetById(int id); +} +``` + +### Custom Data Service Methods + +```csharp +public interface IProductService : IDataServiceWithIntId +{ + // Business logic methods + Task AdjustStock(int productId, int quantity); + Task> GetFeaturedProducts(); + Task DiscontinueProduct(int productId); + Task CalculateTotalInventoryValue(); +} + +public class ProductService : BaseDataServiceWithIntId, IProductService +{ + private new readonly IProductRepository repository; + + public ProductService( + IRepositoryWithIntId repository, + ILogger> logger) + : base(repository, logger) + { + this.repository = (IProductRepository)repository; + } + + public async Task AdjustStock(int productId, int quantity) + { + try + { + logger.LogInformation($"Adjusting stock for product {productId} by {quantity}"); + + var product = await repository.GetById(productId); + if (product == null) + return false; + + product.Stock += quantity; + + if (product.Stock < 0) + throw new InvalidOperationException("Insufficient stock"); + + await repository.Update(product); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, $"Error adjusting stock for product {productId}"); + throw; + } + } + + public async Task> GetFeaturedProducts() + { + return await repository.GetLowStockProducts(10); + } + + public async Task DiscontinueProduct(int productId) + { + var product = await repository.GetById(productId); + if (product == null) + return false; + + product.IsDeleted = true; + await repository.Update(product); + return true; + } + + public async Task CalculateTotalInventoryValue() + { + var products = repository.GetAll(); + return await products + .Where(p => !p.IsDeleted) + .SumAsync(p => p.Price * p.Stock); + } +} +``` + +--- + +## Database Factory & Unit of Work + +### Database Factory + +Implement `IDatabaseFactory` for DbContext management: + +```csharp +using Core.Common; +using Core.Common.Contracts; +using Microsoft.EntityFrameworkCore; + +public interface IAppDatabaseFactory : IDatabaseFactory +{ +} + +public class AppDatabaseFactory : BaseDatabaseFactory, IAppDatabaseFactory +{ + public AppDatabaseFactory(IDbContextFactory contextFactory) + : base(contextFactory) + { + } +} + +// Register in DI +builder.Services.AddScoped(); +``` + +### Unit of Work + +Implement `IUnitOfWork` for transaction management: + +```csharp +using Core.Common.Contracts; +using Microsoft.EntityFrameworkCore; + +public interface IAppUnitOfWork : IUnitOfWork +{ + IProductRepository Products { get; } + IOrderRepository Orders { get; } +} + +public class AppUnitOfWork : IAppUnitOfWork +{ + private readonly AppDbContext _context; + private readonly IProductRepository _productRepository; + private readonly IOrderRepository _orderRepository; + + public AppUnitOfWork( + IAppDatabaseFactory databaseFactory, + IProductRepository productRepository, + IOrderRepository orderRepository) + { + _context = databaseFactory.Get(); + _productRepository = productRepository; + _orderRepository = orderRepository; + } + + public IProductRepository Products => _productRepository; + public IOrderRepository Orders => _orderRepository; + + public async Task CommitAsync() + { + return await _context.SaveChangesAsync(); + } + + public void Dispose() + { + _context?.Dispose(); + } +} + +// Usage +public class OrderService +{ + private readonly IAppUnitOfWork _unitOfWork; + + public OrderService(IAppUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task CreateOrderWithStockUpdate(Order order, int productId, int quantity) + { + // Add order + var createdOrder = await _unitOfWork.Orders.Add(order, commit: false); + + // Update product stock + var product = await _unitOfWork.Products.GetById(productId); + product.Stock -= quantity; + await _unitOfWork.Products.Update(product, commit: false); + + // Commit both changes together + await _unitOfWork.CommitAsync(); + + return createdOrder; + } +} +``` + +--- + +## Data Models + +### Base Model Properties + +All base models include: + +```csharp +public abstract class BaseModel +{ + public bool IsDeleted { get; set; } // Soft delete flag + public DateTime Created { get; set; } // Creation timestamp + public string CreatedBy { get; set; } // Creator identifier + public DateTime? LastUpdated { get; set; } // Last update timestamp + public string LastUpdatedBy { get; set; } // Last updater identifier +} +``` + +### ID-Specific Models + +```csharp +// Integer ID +public class BaseModelWithIntId : BaseModel, IModelWithId +{ + public int Id { get; set; } +} + +// GUID ID +public class BaseModelWithGuidId : BaseModel, IModelWithGuid +{ + public Guid Id { get; set; } = Guid.NewGuid(); +} + +// String ID +public class BaseModelWithStringId : BaseModel, IModelWithStringId +{ + public string Id { get; set; } +} +``` + +### Lookup Models + +For simple lookup/reference data: + +```csharp +public class BaseLookupModel : BaseModelWithIntId, ILookupModel +{ + public string Code { get; set; } + public string Description { get; set; } +} + +// Example usage +public class Category : BaseLookupModel +{ + // Inherits: Id, Code, Description, IsDeleted, Created, etc. +} +``` + +### Custom Models + +```csharp +public class Product : BaseModelWithIntId +{ + public string Name { get; set; } + public string SKU { get; set; } + public decimal Price { get; set; } + public int CategoryId { get; set; } + + // Navigation property + public Category Category { get; set; } +} + +public class Order : BaseModelWithGuidId +{ + public string OrderNumber { get; set; } + public DateTime OrderDate { get; set; } + public decimal Total { get; set; } + + // Collection navigation + public List Items { get; set; } +} + +public class OrderItem : BaseModelWithIntId +{ + public Guid OrderId { get; set; } + public int ProductId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + + // Navigation properties + public Order Order { get; set; } + public Product Product { get; set; } +} +``` + +--- + +## REST Query Integration + +SilverCodeAPI integrates seamlessly with REST-Parser. See [Rest-Parser-Usage.md](Rest-Parser-Usage.md) for full query syntax. + +### Basic Queries + +```csharp +// Simple search +var result = productService.Search("category=Electronics"); + +// With operators +var result = productService.Search("price[lt]=1000&stock[gt]=0"); + +// With sorting +var result = productService.Search("category=Electronics&$sort_by=price[ASC]"); + +// With pagination +var result = productService.Search("isActive=true&$page=1&$pagesize=20"); +``` + +### ApiResult Structure + +```csharp +public class ApiResult +{ + public IEnumerable Data { get; set; } + public Pagination? Pagination { get; set; } +} + +public class Pagination +{ + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int PageCount { get; set; } + public int TotalCount { get; set; } +} +``` + +### Controller Example + +```csharp +[HttpGet] +public IActionResult Search([FromQuery] string q = "") +{ + try + { + var result = _productService.Search(q ?? "$pagesize=20"); + + var response = new + { + data = result.Data, + page = result.Pagination?.PageNumber ?? 1, + pageSize = result.Pagination?.PageSize ?? result.Data.Count(), + totalCount = result.Pagination?.TotalCount ?? result.Data.Count(), + totalPages = result.Pagination?.PageCount ?? 1 + }; + + return Ok(response); + } + catch (REST_InvalidFieldnameException ex) + { + return BadRequest(new { error = "Invalid field", message = ex.Message }); + } + catch (REST_InvalidOperatorException ex) + { + return BadRequest(new { error = "Invalid operator", message = ex.Message }); + } + catch (REST_InvalidValueException ex) + { + return BadRequest(new { error = "Invalid value", message = ex.Message }); + } +} +``` + +### Query Examples + +```http +# Get all electronics under $1000, sorted by price +GET /api/products?category=Electronics&price[lt]=1000&$sort_by=price[ASC] + +# Get page 2 of active products with stock +GET /api/products?isActive=true&stock[gt]=0&$page=2&$pagesize=25 + +# Search by name containing "Pro" +GET /api/products?name[contains]=Pro&$sort_by=price[DESC] + +# Complex query with multiple conditions +GET /api/products?category=Electronics&price[ge]=100&price[le]=500&stock[gt]=5&$sort_by=name[ASC]&$page=1&$pagesize=10 +``` + +--- + +## Auditing + +### Built-in Audit Fields + +All entities automatically track: + +```csharp +public class Product : BaseModelWithIntId +{ + // Your properties + public string Name { get; set; } + + // Inherited audit properties: + // - DateTime Created + // - string CreatedBy + // - DateTime? LastUpdated + // - string LastUpdatedBy + // - bool IsDeleted +} +``` + +### Custom Auditor + +Implement `IAuditor` to populate audit fields: + +```csharp +using Core.Common; +using Core.Common.Contracts; +using Microsoft.AspNetCore.Http; + +public class UserAuditor : BaseAuditor +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public UserAuditor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public override string GetCurrentUser() + { + return _httpContextAccessor.HttpContext?.User?.Identity?.Name ?? "System"; + } +} + +// Register in DI +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); +``` + +### Using Auditor in Repository + +```csharp +public class ProductRepository : BaseRepositoryWithIntId +{ + private readonly IAuditor _auditor; + + public ProductRepository( + IDbContextFactory dbContextFactory, + IRestToLinqParser parser, + ILogger> logger, + IAuditor auditor) + : base(dbContextFactory, parser, logger) + { + _auditor = auditor; + } + + public override async Task Add(Product entity, bool commit = true) + { + entity.CreatedBy = _auditor.GetCurrentUser(); + entity.Created = DateTime.UtcNow; + + return await base.Add(entity, commit); + } + + public override async Task Update(Product entity, bool commit = true) + { + entity.LastUpdatedBy = _auditor.GetCurrentUser(); + entity.LastUpdated = DateTime.UtcNow; + + return await base.Update(entity, commit); + } +} +``` + +### Soft Delete + +```csharp +// Instead of hard delete, use soft delete +public async Task SoftDelete(int productId) +{ + var product = await repository.GetById(productId); + if (product == null) + return false; + + product.IsDeleted = true; + product.LastUpdatedBy = _auditor.GetCurrentUser(); + product.LastUpdated = DateTime.UtcNow; + + await repository.Update(product); + return true; +} + +// Filter out deleted items in queries +public IQueryable GetActiveProducts() +{ + return repository.GetAll().Where(p => !p.IsDeleted); +} +``` + +--- + +## Complete Examples + +### E-Commerce Product Management + +```csharp +// Models +public class Product : BaseModelWithIntId +{ + public string SKU { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } + public int CategoryId { get; set; } + public bool IsFeatured { get; set; } + + public Category Category { get; set; } +} + +public class Category : BaseLookupModel +{ + public List Products { get; set; } +} + +// Repository +public interface IProductRepository : IRepositoryWithIntId +{ + Task> GetFeaturedProducts(); + Task> GetLowStockProducts(int threshold); +} + +public class ProductRepository : BaseRepositoryWithIntId, IProductRepository +{ + public ProductRepository( + IDbContextFactory dbContextFactory, + IRestToLinqParser parser, + ILogger> logger) + : base(dbContextFactory, parser, logger) + { + } + + public async Task> GetFeaturedProducts() + { + return await Task.FromResult( + dbset.Where(p => p.IsFeatured && !p.IsDeleted) + .Include(p => p.Category) + ); + } + + public async Task> GetLowStockProducts(int threshold) + { + return await Task.FromResult( + dbset.Where(p => p.Stock < threshold && !p.IsDeleted) + ); + } +} + +// Service +public interface IProductService : IDataServiceWithIntId +{ + Task> GetFeaturedProducts(); + Task UpdateStock(int productId, int newStock); + Task> GetProductsNeedingRestock(int threshold); +} + +public class ProductService : BaseDataServiceWithIntId, IProductService +{ + private new readonly IProductRepository repository; + + public ProductService( + IRepositoryWithIntId repository, + ILogger> logger) + : base(repository, logger) + { + this.repository = (IProductRepository)repository; + } + + public async Task> GetFeaturedProducts() + { + return await repository.GetFeaturedProducts(); + } + + public async Task UpdateStock(int productId, int newStock) + { + var product = await repository.GetById(productId); + if (product == null) + return false; + + product.Stock = newStock; + await repository.Update(product); + return true; + } + + public async Task> GetProductsNeedingRestock(int threshold) + { + return await repository.GetLowStockProducts(threshold); + } +} + +// Controller +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IProductService _productService; + + public ProductsController(IProductService productService) + { + _productService = productService; + } + + [HttpGet] + public IActionResult Search([FromQuery] string q = "") + { + var result = _productService.Search(q ?? "$pagesize=20"); + return Ok(new { data = result.Data, pagination = result.Pagination }); + } + + [HttpGet("featured")] + public async Task GetFeatured() + { + var products = await _productService.GetFeaturedProducts(); + return Ok(products); + } + + [HttpGet("low-stock")] + public async Task GetLowStock([FromQuery] int threshold = 10) + { + var products = await _productService.GetProductsNeedingRestock(threshold); + return Ok(products); + } + + [HttpGet("{id}")] + public async Task Get(int id) + { + var product = await _productService.GetById(id); + return product != null ? Ok(product) : NotFound(); + } + + [HttpPost] + public async Task Create([FromBody] Product product) + { + var created = await _productService.Add(product); + return CreatedAtAction(nameof(Get), new { id = created.Id }, created); + } + + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Product product) + { + if (id != product.Id) + return BadRequest(); + + var updated = await _productService.Update(product); + return Ok(updated); + } + + [HttpPatch("{id}/stock")] + public async Task UpdateStock(int id, [FromBody] int stock) + { + var success = await _productService.UpdateStock(id, stock); + return success ? NoContent() : NotFound(); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _productService.Delete(p => p.Id == id); + return success ? NoContent() : NotFound(); + } +} +``` + +--- + +## Best Practices + +### 1. Use DbContextFactory + +Always use `IDbContextFactory` for proper DbContext lifecycle management: + +```csharp +// ✅ GOOD +builder.Services.AddDbContextFactory(options => + options.UseSqlServer(connectionString)); + +// ❌ AVOID +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); +``` + +### 2. Prefer UTC Dates + +```csharp +// ✅ GOOD +entity.Created = DateTime.UtcNow; +entity.LastUpdated = DateTime.UtcNow; + +// ❌ AVOID +entity.Created = DateTime.Now; +``` + +### 3. Use Async Properly + +```csharp +// ✅ GOOD +public async Task GetProductAsync(int id) +{ + return await repository.GetById(id); +} + +// ❌ AVOID +public Product GetProduct(int id) +{ + return repository.GetById(id).Result; +} +``` + +### 4. Implement Read-Only Repositories + +```csharp +// For queries that don't need write access +public class ProductQueryService +{ + private readonly IReadRepositoryWithIntId _readOnlyRepo; + + public ProductQueryService(IReadRepositoryWithIntId readOnlyRepo) + { + _readOnlyRepo = readOnlyRepo; + } +} +``` + +### 5. Use Unit of Work for Transactions + +```csharp +public async Task CreateOrderWithInventoryUpdate(CreateOrderDto dto) +{ + using var unitOfWork = _unitOfWorkFactory.Create(); + + try + { + // Create order + var order = await unitOfWork.Orders.Add(orderEntity, commit: false); + + // Update inventory + foreach (var item in dto.Items) + { + var product = await unitOfWork.Products.GetById(item.ProductId); + product.Stock -= item.Quantity; + await unitOfWork.Products.Update(product, commit: false); + } + + // Commit all changes together + await unitOfWork.CommitAsync(); + + return order; + } + catch + { + // Rollback is automatic + throw; + } +} +``` + +### 6. Leverage REST Query Validation + +```csharp +[HttpGet] +public IActionResult Search([FromQuery] string q = "") +{ + try + { + // Provide sensible defaults + if (string.IsNullOrWhiteSpace(q)) + q = "$sort_by=Id&$pagesize=20"; + + var result = _service.Search(q); + return Ok(result); + } + catch (REST_InvalidFieldnameException ex) + { + return BadRequest(new { error = "Invalid field", field = ex.Message }); + } + catch (REST_InvalidOperatorException ex) + { + return BadRequest(new { error = "Invalid operator", details = ex.Message }); + } + catch (REST_InvalidValueException ex) + { + return BadRequest(new { error = "Invalid value", details = ex.Message }); + } +} +``` + +### 7. Use DTOs for API Responses + +```csharp +// Don't expose entities directly +public class ProductDto +{ + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public int Stock { get; set; } + // Exclude: CreatedBy, LastUpdatedBy, IsDeleted +} + +[HttpGet("{id}")] +public async Task Get(int id) +{ + var product = await _productService.GetById(id); + if (product == null) + return NotFound(); + + var dto = new ProductDto + { + Id = product.Id, + Name = product.Name, + Price = product.Price, + Stock = product.Stock + }; + + return Ok(dto); +} +``` + +### 8. Implement Proper Logging + +```csharp +public class ProductService : BaseDataServiceWithIntId +{ + public override async Task Add(Product model) + { + try + { + logger.LogInformation("Adding new product: {ProductName}", model.Name); + var result = await base.Add(model); + logger.LogInformation("Successfully added product with ID: {ProductId}", result.Id); + return result; + } + catch (Exception ex) + { + logger.LogError(ex, "Error adding product: {ProductName}", model.Name); + throw; + } + } +} +``` + +--- + +## Migration Guide + +### From Direct EF Core to SilverCodeAPI + +#### Before (Direct EF Core) + +```csharp +public class ProductsController : ControllerBase +{ + private readonly AppDbContext _context; + + public ProductsController(AppDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task GetProducts() + { + var products = await _context.Products.ToListAsync(); + return Ok(products); + } + + [HttpPost] + public async Task CreateProduct(Product product) + { + _context.Products.Add(product); + await _context.SaveChangesAsync(); + return Ok(product); + } +} +``` + +#### After (SilverCodeAPI) + +```csharp +public class ProductsController : ControllerBase +{ + private readonly IProductService _productService; + + public ProductsController(IProductService productService) + { + _productService = productService; + } + + [HttpGet] + public IActionResult GetProducts([FromQuery] string q = "") + { + var result = _productService.Search(q ?? "$pagesize=20"); + return Ok(result); + } + + [HttpPost] + public async Task CreateProduct(Product product) + { + var created = await _productService.Add(product); + return Ok(created); + } +} +``` + +### Migration Steps + +1. **Add NuGet Packages** +2. **Update Entity Models** - Inherit from appropriate base models +3. **Create Repositories** - Implement repository interfaces +4. **Create Data Services** - Implement service layer +5. **Update DI Registration** - Register new services +6. **Update Controllers** - Inject services instead of DbContext +7. **Update Queries** - Use REST query syntax + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: "DbContext has been disposed" + +**Cause**: DbContext lifecycle mismatch + +**Solution**: Ensure you're using `IDbContextFactory`: + +```csharp +builder.Services.AddDbContextFactory(options => + options.UseSqlServer(connectionString)); +``` + +#### Issue: "REST parser not found" + +**Cause**: REST parser not registered for entity type + +**Solution**: Register parser in DI: + +```csharp +builder.Services.RegisterRestParser(); +``` + +#### Issue: "Navigation properties are null" + +**Cause**: Include not specified + +**Solution**: Use `includeChildren` parameter: + +```csharp +var product = await repository.GetById(id, includeChildren: true); +``` + +#### Issue: "Audit fields are null" + +**Cause**: Auditor not implemented or registered + +**Solution**: Implement and register auditor: + +```csharp +builder.Services.AddScoped(); +``` + +#### Issue: "Pagination not working" + +**Cause**: Missing pagination parameters in query + +**Solution**: Include `$page` and `$pagesize`: + +```csharp +var result = service.Search("category=Electronics&$page=1&$pagesize=20"); +``` + +--- + +## Additional Resources + +- **REST-Parser Documentation**: [Rest-Parser-Usage.md](Rest-Parser-Usage.md) +- **GitHub Repository**: https://github.com/BigBadJock/SilverCodeAPI +- **NuGet Packages**: + - https://www.nuget.org/packages/Core.Common.Contracts/ + - https://www.nuget.org/packages/Core.Common.DataModels/ + - https://www.nuget.org/packages/Core.Common/ + +--- + +**Last Updated**: 2025 +**Library Version**: 1.2025.* +**Target Framework**: .NET 10 +**Author**: John McArthur diff --git a/SilverCodeAPI.sln b/SilverCodeAPI.sln index 6381a2b..c04d165 100644 --- a/SilverCodeAPI.sln +++ b/SilverCodeAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32228.430 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11620.152 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Common.Contracts", "Core.Common.Contracts\Core.Common.Contracts.csproj", "{7659748C-F934-4C6D-B13E-D171D1363FB0}" EndProject @@ -20,6 +20,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{E66DFF0B-8138-4742-97B2-6848054862A2}" + ProjectSection(SolutionItems) = preProject + Docs\Rest-Parser-Usage.md = Docs\Rest-Parser-Usage.md + Docs\SilverCodeAPI-Usage-Guide.md = Docs\SilverCodeAPI-Usage-Guide.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU