Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions CSharpEssentials.Validation/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -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)**

---

Expand All @@ -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) |

Expand Down
33 changes: 15 additions & 18 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public User GetUser(Guid id)
public Result<User> 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);
}

Expand All @@ -94,11 +94,11 @@ Model the absence of a value without null references.
string display = user?.Profile?.DisplayName?.Trim()?.ToUpper() ?? "ANONYMOUS";

// ✅ Maybe<T>: 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\<T1,T2,...\> — Discriminated Unions
Expand All @@ -108,7 +108,7 @@ A value that is exactly one of several types — exhaustively matched at compile
```csharp
Any<Circle, Rectangle, Triangle> shape = new Circle(radius: 5);

double area = shape.Match(
AnyActionResult<double> area = shape.Match(
circle => Math.PI * circle.Radius * circle.Radius,
rectangle => rectangle.Width * rectangle.Height,
triangle => 0.5 * triangle.Base * triangle.Height
Expand All @@ -121,20 +121,17 @@ double area = shape.Match(
Build complex validation logic from simple, reusable rules that return `Result<T>`.

```csharp
var minAge = Rule.Create<User>(u => u.Age >= 18, Error.Validation("Must be 18+"));
var validEmail = Rule.Create<User>(u => u.Email.Contains('@'), Error.Validation("Invalid email"));
var verified = Rule.Create<User>(u => u.IsVerified, Error.Validation("Account not verified"));
Func<User, Result> minAge = u => u.Age >= 18 ? Result.Success() : Result.Failure(Error.Validation("Age.MinAge", "Must be 18+"));
Func<User, Result> validEmail = u => u.Email.Contains('@') ? Result.Success() : Result.Failure(Error.Validation("Email.Invalid", "Invalid email"));
Func<User, Result> verified = u => u.IsVerified ? Result.Success() : Result.Failure(Error.Validation("Account.NotVerified", "Account not verified"));

// AND — all rules must pass, collects every failure
Result<User> result = RuleEngine
.Create(minAge, validEmail, verified)
.Evaluate(user);
Result<User> result = RuleEngine.And(
new IRule<User>[] { minAge.ToRule(), validEmail.ToRule(), verified.ToRule() }, user);

// OR — at least one rule must pass
Result<User> adminOrMod = RuleEngine
.Create(isAdmin, isModerator)
.Or()
.Evaluate(user);
Result<User> adminOrMod = RuleEngine.Or(
new IRule<User>[] { isAdmin.ToRule(), isModerator.ToRule() }, user);
```

### AspNetCore — Result → HTTP, Automatically
Expand All @@ -143,14 +140,14 @@ Map `Result<T>` 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<T>, 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
Expand Down
20 changes: 15 additions & 5 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` forces every caller to handle both paths, enables method chaining that short-circuits on failure, and makes error accumulation trivial.
Expand All @@ -113,9 +115,17 @@ Two core types: `Result` (no value, just success/failure) and `Result<T>` (carri
| `Result.Failure(errors)` | `Result` | Multiple errors failure |
| `Result<T>.Failure(error)` | `Result<T>` | Typed failure |
| `Result.SuccessIf(condition, error)` | `Result` | Guard clause — success if condition holds |
| `Result.SuccessIf(condition, value, error)` | `Result<T>` | 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<T>` | Typed try/catch wrapper |
| `Result.FailureIf<TValue>(condition, error)` | `Result<T>` | 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<T>` | Wraps try/catch around `Func<T>`, returns value on success |
| `Result.Try(func, handler)` | `Result<T>` | Wraps try/catch around `Func<Result<T>>`, propagates inner result |
| `Result.Try(func, handler)` | `Result` | Wraps try/catch around `Func<Result>`, propagates inner result |
| `Result.TryAsync(action, handler)` | `Task<Result>` | Async try/catch around `Func<Task>` |
| `Result.TryAsync(func, handler)` | `Task<Result<T>>` | Async try/catch around `Func<Task<T>>` |
| `Result.TryAsync(func, handler)` | `Task<Result<T>>` | Async try/catch around `Func<Task<Result<T>>>` |
| `Result.TryAsync(func, handler)` | `Task<Result>` | Async try/catch around `Func<Task<Result>>` |
| `Result.From(errors)` | `Result` | Success if errors empty, failure otherwise |
| `Result<int> r = 42;` | `Result<int>` | Implicit operator for ergonomic creation |

Expand Down Expand Up @@ -428,8 +438,8 @@ var (values, missingCount) = maybes.Partition();

| Method | Returns | What It Does |
|--------|---------|-------------|
| `Match(first:, second:, ...)` | `AnyActionResult<T>` | Transforms the active variant — exhaustive |
| `Switch(first:, second:, ...)` | `AnyActionStatus` | Executes action for active variant — exhaustive |
| `Match(first:, second:, ...)` | `AnyActionResult<T>` | 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
Expand Down Expand Up @@ -931,7 +941,7 @@ Result<User> 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` |
Expand Down
8 changes: 4 additions & 4 deletions examples/Examples.Any/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()` to verify the underlying type |
| **Casting** | `As<T>()` with safe fallback to default |
| **Any Type** | Store any value in a single type with `Any.Create<T0,T1>(value)` or implicit operator |
| **Type Checking** | `Is<TTarget, T0, T1>()` to verify the underlying type |
| **Casting** | `As<TTarget, T0, T1>()` with safe fallback to default |
| **AnyActionResult** | Generic success/failure result with any payload |
| **AnyT2 – AnyT8** | Typed unions for 2 to 8 different types |
| **Any&lt;T0,T1&gt; through Any&lt;T0,...,T7&gt;** | Typed unions for 2 to 8 different types |

## Running

Expand Down
6 changes: 3 additions & 3 deletions examples/Examples.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 13 additions & 14 deletions examples/Examples.EntityFrameworkCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,16 @@ The demo performs the following steps automatically:
`ShopDbContext` inherits from `BaseDbContext`:

```csharp
public class ShopDbContext : BaseDbContext
public class ShopDbContext : BaseDbContext<ShopDbContext>
{
public ShopDbContext(DbContextOptions<ShopDbContext> options) : base(options) { }
public ShopDbContext(
DbContextOptions<ShopDbContext> 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);
}
}
```
Expand Down Expand Up @@ -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<Product> for current page
// page.Items -> IReadOnlyList<Product> for current page
// page.TotalCount -> Total items across all pages
// page.TotalPages -> Total number of pages
// page.HasNextPage -> bool
Expand All @@ -110,7 +109,7 @@ var request = new CursorPaginationRequest<DateTimeOffset>
var response = await _dbContext.Logs
.PaginateAsync(request, cursorSelector: x => x.CreatedAt);

// response.NextCursor -> Continue from here
// response.Next -> Continue from here
```

## Enum String Conversion
Expand Down
5 changes: 2 additions & 3 deletions examples/Examples.Enums/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()`, `ParseEnum<T>`, `GetValues<T>` |
| **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

Expand Down
2 changes: 1 addition & 1 deletion examples/Examples.Json/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()`, `ConvertFromJson<T>()`, `ConvertToJsonDocument<T>()` |
| **DateTime Conversion** | Multi-format date parsing support |

## Running
Expand Down
40 changes: 9 additions & 31 deletions examples/Examples.RequestResponseLogging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,41 +93,19 @@ curl -s https://localhost:5001/health
## Configuration Options

```csharp
builder.Services.AddRequestResponseLogging(options =>
app.AddRequestResponseLogging(opt =>
{
options.UseLogWriter<StructuredJsonLogWriter>();

// 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<RequestResponseContext, Task> handler)` kullan.

## Security Considerations

Expand Down
2 changes: 1 addition & 1 deletion examples/Examples.Rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` |
| **Simple Rules** | Create individual validation rules with `Func<T, Result>.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 |
Expand Down
3 changes: 1 addition & 2 deletions examples/Examples.Time/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading