diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bcd46f..cf17d1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,30 @@ We love your input! We want to make contributing to CSharpEssentials as easy and By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). +## Getting Started + +### Prerequisites + +- **.NET SDK**: .NET 9 SDK (or .NET 8 SDK for netstandard targets) +- **Git**: For version control + +### First-Time Setup + +```bash +# Clone your fork +git clone https://github.com/senrecep/CSharpEssentials.git +cd CSharpEssentials + +# Configure git hooks (enables pre-commit badge validation) +git config core.hooksPath .githooks + +# Build the solution +dotnet build + +# Run the test suite +dotnet test +``` + ## Development Workflow Here's how you can contribute to the project using Git: @@ -81,7 +105,7 @@ Here's how you can contribute to the project using Git: We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. -## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html) +## We Use [Github Flow](https://docs.github.com/en/get-started/using-github/github-flow) Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: diff --git a/CSharpEssentials.Validation/BENCHMARKS.md b/CSharpEssentials.Validation/BENCHMARKS.md index dfce84e..29ee6d8 100644 --- a/CSharpEssentials.Validation/BENCHMARKS.md +++ b/CSharpEssentials.Validation/BENCHMARKS.md @@ -1,6 +1,6 @@ # CSharpEssentials.Validation — Performance Benchmarks -**vs. FluentValidation 11.x · BenchmarkDotNet v0.14.0 · Apple M3 Pro · .NET 9 / 10 / 11** +**vs. FluentValidation 11.x · BenchmarkDotNet v0.14.0 · Apple M3 Pro · .NET 9 / 10 / 11 (preview)** --- @@ -21,7 +21,7 @@ validation requirements, exercising equivalent logic on identical input data. | Machine | Apple M3 Pro — 12 cores (arm64) | | OS | macOS 26.3.1 / Darwin 25.3.0 | | Tool | BenchmarkDotNet v0.14.0, [MemoryDiagnoser] | -| Runtimes tested | .NET 9.0.4, .NET 10.0.7, .NET 11.0.0 | +| Runtimes tested | .NET 9.0.4, .NET 10.0.7, .NET 11.0.0-preview.X | | FluentValidation | 11.11.0 | | CSharpEssentials.Validation | 3.0.x (lazy Configure + Span-based property parsing + lazy error list) | diff --git a/README.MD b/README.MD index 1b40eb6..cd39116 100644 --- a/README.MD +++ b/README.MD @@ -74,7 +74,7 @@ public User GetUser(Guid id) public Result GetUser(Guid id) { var user = _db.Find(id); - if (user is null) return Error.NotFound("User not found"); + if (user is null) return Error.NotFound("User.NotFound", "User not found"); return Result.Success(user); } @@ -94,11 +94,11 @@ Model the absence of a value without null references. string display = user?.Profile?.DisplayName?.Trim()?.ToUpper() ?? "ANONYMOUS"; // ✅ Maybe: pipeline that handles absence at every step -string display = Maybe.Return(user) - .Bind(u => Maybe.Return(u.Profile)) - .Bind(p => Maybe.Return(p.DisplayName)) +string display = Maybe.From(user) + .Bind(u => Maybe.From(u.Profile)) + .Bind(p => Maybe.From(p.DisplayName)) .Map(n => n.Trim().ToUpper()) - .Fallback("ANONYMOUS"); + .GetValueOrDefault("ANONYMOUS"); ``` ### Any\ — Discriminated Unions @@ -108,7 +108,7 @@ A value that is exactly one of several types — exhaustively matched at compile ```csharp Any shape = new Circle(radius: 5); -double area = shape.Match( +AnyActionResult area = shape.Match( circle => Math.PI * circle.Radius * circle.Radius, rectangle => rectangle.Width * rectangle.Height, triangle => 0.5 * triangle.Base * triangle.Height @@ -121,20 +121,17 @@ double area = shape.Match( Build complex validation logic from simple, reusable rules that return `Result`. ```csharp -var minAge = Rule.Create(u => u.Age >= 18, Error.Validation("Must be 18+")); -var validEmail = Rule.Create(u => u.Email.Contains('@'), Error.Validation("Invalid email")); -var verified = Rule.Create(u => u.IsVerified, Error.Validation("Account not verified")); +Func minAge = u => u.Age >= 18 ? Result.Success() : Result.Failure(Error.Validation("Age.MinAge", "Must be 18+")); +Func validEmail = u => u.Email.Contains('@') ? Result.Success() : Result.Failure(Error.Validation("Email.Invalid", "Invalid email")); +Func verified = u => u.IsVerified ? Result.Success() : Result.Failure(Error.Validation("Account.NotVerified", "Account not verified")); // AND — all rules must pass, collects every failure -Result result = RuleEngine - .Create(minAge, validEmail, verified) - .Evaluate(user); +Result result = RuleEngine.And( + new IRule[] { minAge.ToRule(), validEmail.ToRule(), verified.ToRule() }, user); // OR — at least one rule must pass -Result adminOrMod = RuleEngine - .Create(isAdmin, isModerator) - .Or() - .Evaluate(user); +Result adminOrMod = RuleEngine.Or( + new IRule[] { isAdmin.ToRule(), isModerator.ToRule() }, user); ``` ### AspNetCore — Result → HTTP, Automatically @@ -143,14 +140,14 @@ Map `Result` to correct HTTP status codes without writing any error-mapping b ```csharp // Program.cs -app.AddCSharpEssentials(); // registers GlobalExceptionHandler + ProblemDetails +builder.Services.AddEnhancedProblemDetails(); // registers ProblemDetails // Controller — business logic returns Result, HTTP mapping is automatic [HttpGet("{id:guid}")] public IActionResult GetProduct(Guid id) => _productService.GetProduct(id).Match( onSuccess: product => Ok(product), - onFailure: errors => errors.ToActionResult() // 404 / 400 / 409 based on ErrorType + onError: errors => errors.ToActionResult() // 404 / 400 / 409 based on ErrorType ); // Service — chain multiple steps, first failure short-circuits diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index bef8eb1..ae613a7 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -97,6 +97,8 @@ Error[] allErrors = validationError + conflictError; ## 2. CSharpEssentials.Results — Railway-Oriented Programming +> **Note:** The NuGet package is named `CSharpEssentials.Results`, but the actual C# namespace is `CSharpEssentials.ResultPattern`. Add `using CSharpEssentials.ResultPattern;` in your code. + **What it is:** A Result monad that makes success and failure explicit in your type signatures. **Why it exists:** Traditional C# uses exceptions for flow control and null for absence — both are invisible in method signatures and break composability. `Result` forces every caller to handle both paths, enables method chaining that short-circuits on failure, and makes error accumulation trivial. @@ -113,9 +115,17 @@ Two core types: `Result` (no value, just success/failure) and `Result` (carri | `Result.Failure(errors)` | `Result` | Multiple errors failure | | `Result.Failure(error)` | `Result` | Typed failure | | `Result.SuccessIf(condition, error)` | `Result` | Guard clause — success if condition holds | +| `Result.SuccessIf(condition, value, error)` | `Result` | Guard clause — success with value if condition holds | | `Result.FailureIf(condition, error)` | `Result` | Guard clause — failure if condition holds | -| `Result.Try(action, handler)` | `Result` | Wraps try/catch, converts exception to Error | -| `Result.Try(func, handler)` | `Result` | Typed try/catch wrapper | +| `Result.FailureIf(condition, error)` | `Result` | Guard clause — typed failure if condition holds | +| `Result.Try(action, handler)` | `Result` | Wraps try/catch around `Action`, converts exception to Error | +| `Result.Try(func, handler)` | `Result` | Wraps try/catch around `Func`, returns value on success | +| `Result.Try(func, handler)` | `Result` | Wraps try/catch around `Func>`, propagates inner result | +| `Result.Try(func, handler)` | `Result` | Wraps try/catch around `Func`, propagates inner result | +| `Result.TryAsync(action, handler)` | `Task` | Async try/catch around `Func` | +| `Result.TryAsync(func, handler)` | `Task>` | Async try/catch around `Func>` | +| `Result.TryAsync(func, handler)` | `Task>` | Async try/catch around `Func>>` | +| `Result.TryAsync(func, handler)` | `Task` | Async try/catch around `Func>` | | `Result.From(errors)` | `Result` | Success if errors empty, failure otherwise | | `Result r = 42;` | `Result` | Implicit operator for ergonomic creation | @@ -428,8 +438,8 @@ var (values, missingCount) = maybes.Partition(); | Method | Returns | What It Does | |--------|---------|-------------| -| `Match(first:, second:, ...)` | `AnyActionResult` | Transforms the active variant — exhaustive | -| `Switch(first:, second:, ...)` | `AnyActionStatus` | Executes action for active variant — exhaustive | +| `Match(first:, second:, ...)` | `AnyActionResult` | Transforms the active variant — partial (delegates are optional) | +| `Switch(first:, second:, ...)` | `AnyActionStatus` | Executes action for active variant — partial (delegates are optional) | | `Deconstruct(out index, out value)` | void | C# deconstruction support | ```csharp @@ -931,7 +941,7 @@ Result user = await ResiliencePolicy | Behavior | Marker Interface | What It Does | |----------|-----------------|-------------| -| `ValidationBehavior` | — (auto for all) | Runs FluentValidation before handler; returns `Result.Failure` with validation errors | +| `ValidationBehavior` | — (auto for all) | Runs CSharpEssentials.Validation before handler; returns `Result.Failure` with validation errors | | `LoggingBehavior` | `ILoggableRequest` | Logs request/response details | | `CachingBehavior` | `ICacheable` | Caches handler responses using `CacheKey` and `CacheDuration` | | `TransactionScopeBehavior` | `ITransactionalRequest` | Wraps handler execution in `TransactionScope` | diff --git a/examples/Examples.Any/README.md b/examples/Examples.Any/README.md index 91c84c7..20a8492 100644 --- a/examples/Examples.Any/README.md +++ b/examples/Examples.Any/README.md @@ -6,11 +6,11 @@ This console application demonstrates the dynamic Any type from `CSharpEssential | Feature | Description | |---------|-------------| -| **Any Type** | Store any value in a single type with `Any.From()` | -| **Type Checking** | `Is()` to verify the underlying type | -| **Casting** | `As()` with safe fallback to default | +| **Any Type** | Store any value in a single type with `Any.Create(value)` or implicit operator | +| **Type Checking** | `Is()` to verify the underlying type | +| **Casting** | `As()` with safe fallback to default | | **AnyActionResult** | Generic success/failure result with any payload | -| **AnyT2 – AnyT8** | Typed unions for 2 to 8 different types | +| **Any<T0,T1> through Any<T0,...,T7>** | Typed unions for 2 to 8 different types | ## Running diff --git a/examples/Examples.Core/README.md b/examples/Examples.Core/README.md index 7bb3c3e..0b83152 100644 --- a/examples/Examples.Core/README.md +++ b/examples/Examples.Core/README.md @@ -6,11 +6,11 @@ This console application demonstrates the core utility extensions provided by `C | Feature | Description | |---------|-------------| -| **Guid Generation** | `Guider.NewGuid()`, `NewGuidAsString()`, URL-safe encoding | +| **Guid Generation** | `Guider.NewGuid()`, `Guider.ToStringFromGuid()`, `Guider.ToGuidFromString()` | | **String Extensions** | `ToPascalCase`, `ToCamelCase`, `ToSnakeCase`, `ToKebabCase`, `ToTrainCase`, `ToTitleCase`, `IsNumeric` (with optional `CultureInfo`) | -| **Collection Extensions** | `IsNullOrEmpty`, `IsNotNullOrEmpty`, `AddRange` with params | +| **Collection Extensions** | `IfAdd`, `IfAddRange`, `HasSameElements`, `AllTrue`, `AllFalse`, `WithoutNulls` | | **General Extensions** | `IsNull`, `IsNotNull`, `AsValueTask`, `In` | -| **Random Items** | `RandomItem`, `RandomItems` from collections | +| **Random Items** | `GetRandomItem`, `GetRandomItems` from collections | ## Running diff --git a/examples/Examples.EntityFrameworkCore/README.md b/examples/Examples.EntityFrameworkCore/README.md index fc20af7..046dc79 100644 --- a/examples/Examples.EntityFrameworkCore/README.md +++ b/examples/Examples.EntityFrameworkCore/README.md @@ -39,19 +39,16 @@ The demo performs the following steps automatically: `ShopDbContext` inherits from `BaseDbContext`: ```csharp -public class ShopDbContext : BaseDbContext +public class ShopDbContext : BaseDbContext { - public ShopDbContext(DbContextOptions options) : base(options) { } + public ShopDbContext( + DbContextOptions options, + IServiceScopeFactory serviceScopeFactory) : base(options, serviceScopeFactory) { } - protected override void OnModelCreating(ModelBuilder modelBuilder) + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { - base.OnModelCreating(modelBuilder); - - // Convert table/column names to snake_case automatically - modelBuilder.UseSnakeCaseNamingConvention(); - - // Store enums as snake_case strings (e.g. "electronics") - modelBuilder.UseEnumToStringConversion(StringCase.SnakeCase); + base.ConfigureConventions(configurationBuilder); + configurationBuilder.ConfigureEnumConventions(typeof(ShopDbContext).Assembly); } } ``` @@ -86,11 +83,13 @@ var all = db.Products.IgnoreQueryFilters().ToList(); ### Offset-based ```csharp -var page = _dbContext.Products +var request = new PaginationRequest { PageNumber = 1, PageSize = 10 }; + +var page = await _dbContext.Products .OrderBy(p => p.Name) - .ToPaginatedList(pageNumber: 1, pageSize: 10); + .PaginateAsync(request); -// page.Items -> List for current page +// page.Items -> IReadOnlyList for current page // page.TotalCount -> Total items across all pages // page.TotalPages -> Total number of pages // page.HasNextPage -> bool @@ -110,7 +109,7 @@ var request = new CursorPaginationRequest var response = await _dbContext.Logs .PaginateAsync(request, cursorSelector: x => x.CreatedAt); -// response.NextCursor -> Continue from here +// response.Next -> Continue from here ``` ## Enum String Conversion diff --git a/examples/Examples.Enums/README.md b/examples/Examples.Enums/README.md index 50ff017..8843fbb 100644 --- a/examples/Examples.Enums/README.md +++ b/examples/Examples.Enums/README.md @@ -7,9 +7,8 @@ This console application demonstrates enum utilities from `CSharpEssentials.Enum | Feature | Description | |---------|-------------| | **StringEnumAttribute** | Decorate enum values with string representations | -| **GetStringValue** | Extract the string value from enum members | -| **Enum Extensions** | `GetCount()`, `ParseEnum`, `GetValues` | -| **String Parsing** | Parse enums from their string attribute values | +| **Enum Extensions** | `ToOptimizedString()`, `ToSnakeCase()`, `ToKebabCase()`, `Parse()`, `TryParse()`, `IsDefined()`, `GetNames()`, `GetValues()` | +| **String Parsing** | Parse enums from their member names using source-generated extensions | ## Running diff --git a/examples/Examples.Json/README.md b/examples/Examples.Json/README.md index eb035c5..18e7899 100644 --- a/examples/Examples.Json/README.md +++ b/examples/Examples.Json/README.md @@ -8,7 +8,7 @@ This console application demonstrates JSON utilities from `CSharpEssentials.Json |---------|-------------| | **Default Options** | Pre-configured `JsonSerializerOptions` with common settings | | **Polymorphic Serialization** | Serialize/deserialize derived types with discriminator | -| **Json Extensions** | `IsValidJson`, `ToPrettyJson`, `MinifyJson` | +| **Json Extensions** | `ConvertToJson()`, `ConvertFromJson()`, `ConvertToJsonDocument()` | | **DateTime Conversion** | Multi-format date parsing support | ## Running diff --git a/examples/Examples.RequestResponseLogging/README.md b/examples/Examples.RequestResponseLogging/README.md index cbf1497..38d5771 100644 --- a/examples/Examples.RequestResponseLogging/README.md +++ b/examples/Examples.RequestResponseLogging/README.md @@ -59,7 +59,7 @@ curl -s -X POST https://localhost:5001/api/demo/validation \ curl -s https://localhost:5001/api/demo/slow ``` -### 6. Health Check (excluded from logging by PathFilter) +### 6. Health Check (excluded from logging by IgnorePaths) ```bash curl -s https://localhost:5001/health @@ -93,41 +93,19 @@ curl -s https://localhost:5001/health ## Configuration Options ```csharp -builder.Services.AddRequestResponseLogging(options => +app.AddRequestResponseLogging(opt => { - options.UseLogWriter(); - - // Toggle what gets captured - options.LogRequestBody = true; - options.LogResponseBody = true; - options.LogRequestHeaders = true; - options.LogResponseHeaders = true; - - // Exclude certain paths from logging - options.PathFilter = path => !path.StartsWith("/health"); - - // Limit body size to prevent memory issues - options.MaxBodyLength = 1024 * 64; // 64 KB + opt.IgnorePaths("/health", "/metrics"); + opt.UseLogger(loggerFactory, options => + { + options.LoggingLevel = LogLevel.Information; + }); }); ``` -## Custom Log Writer - -The `ILogWriter` interface allows complete control over how logs are persisted: - -```csharp -public interface ILogWriter -{ - Task WriteAsync(RequestResponseContext context, CancellationToken cancellationToken = default); -} -``` +## Custom Handler -Implementations can write to: -- Console / Structured JSON (shown here) -- Files with rotation -- Databases (SQL Server, PostgreSQL, MongoDB) -- Message queues (RabbitMQ, Kafka, Azure Service Bus) -- Cloud logging services (AWS CloudWatch, Azure Application Insights, GCP Logging) +`ILogWriter` interface'i internal'dır — custom implementasyon için `UseHandler(Func handler)` kullan. ## Security Considerations diff --git a/examples/Examples.Rules/README.md b/examples/Examples.Rules/README.md index 44271de..375b8a0 100644 --- a/examples/Examples.Rules/README.md +++ b/examples/Examples.Rules/README.md @@ -6,7 +6,7 @@ This console application demonstrates the rule engine from `CSharpEssentials.Rul | Feature | Description | |---------|-------------| -| **Simple Rules** | Create individual validation rules with `Rule.Create` | +| **Simple Rules** | Create individual validation rules with `Func.ToRule()` | | **Rule Engine** | Evaluate multiple rules where ALL must pass (AND) | | **Or Rules** | Combine rules where AT LEAST ONE must pass | | **Result Integration** | Rules return `Result` for composable validation | diff --git a/examples/Examples.Time/README.md b/examples/Examples.Time/README.md index 34259a5..96cf165 100644 --- a/examples/Examples.Time/README.md +++ b/examples/Examples.Time/README.md @@ -7,8 +7,7 @@ This console application demonstrates date/time utilities from `CSharpEssentials | Feature | Description | |---------|-------------| | **DateTimeProvider** | Abstraction over `DateTime.UtcNow` for testability | -| **DateTime Extensions** | `StartOfDay`, `EndOfDay`, `StartOfMonth`, `EndOfMonth`, `StartOfWeek`, `ToUnixTimestamp` | -| **TimeZone Conversions** | Convert between UTC and local time | +| **DateTime Extensions** | `ToDateOnly()` (NET6+), `ToTimeOnly()` (NET6+) | | **Custom Provider** | Fixed provider for unit testing | ## Running