Esox.SharpAndRusty.AspNetCore
1.1.1
dotnet add package Esox.SharpAndRusty.AspNetCore --version 1.1.1
NuGet\Install-Package Esox.SharpAndRusty.AspNetCore -Version 1.1.1
<PackageReference Include="Esox.SharpAndRusty.AspNetCore" Version="1.1.1" />
<PackageVersion Include="Esox.SharpAndRusty.AspNetCore" Version="1.1.1" />
<PackageReference Include="Esox.SharpAndRusty.AspNetCore" />
paket add Esox.SharpAndRusty.AspNetCore --version 1.1.1
#r "nuget: Esox.SharpAndRusty.AspNetCore, 1.1.1"
#:package Esox.SharpAndRusty.AspNetCore@1.1.1
#addin nuget:?package=Esox.SharpAndRusty.AspNetCore&version=1.1.1
#tool nuget:?package=Esox.SharpAndRusty.AspNetCore&version=1.1.1
Esox.SharpAndRusty.AspNetCore
ASP.NET Core integration for Esox.SharpAndRusty functional types (Option, Result, Either, Validation).
Features
- ✅ Action Result Conversions - Convert
Result/ExtendedResult/Option/Either/ValidationtoIActionResult - ✅ RFC 7807 ProblemDetails - Automatic conversion of
Errortypes to standardized problem details - ✅ Model Binding - Bind
Option<T>from request parameters/body with full type support - ✅ Global Error Handling - Middleware for catching exceptions and converting to ProblemDetails
- ✅ Automatic Status Codes - ErrorKind automatically maps to appropriate HTTP status codes
- ✅ Validation Integration -
Validation<T, E>converts to ValidationProblemDetails - ✅ JSON Serialization - Clean serialization of
Option<T>,Result<T,E>, andExtendedResult<T,E>in API responses - ✅ Comprehensive Testing - 570 total test executions in the latest verified run; all 570 passing across .NET 8, 9, and 10
Why Use This Library?
✨ Type-Safe API Responses
// Instead of this:
public User? GetUser(int id) // Nullable types lose context
{
var user = _db.Users.Find(id);
return user; // Is null a valid value or an error?
}
// Do this:
public Result<User, Error> GetUser(int id) // Clear success/failure semantics
{
var user = _db.Users.Find(id);
return user != null
? Result<User, Error>.Ok(user)
: Result<User, Error>.Err(Error.New("User not found", ErrorKind.NotFound));
}
// And in your controller:
[HttpGet("{id}")]
public IActionResult Get(int id) => GetUser(id).ToActionResult(); // Automatic HTTP status mapping!
🎯 Optional Parameters Done Right
// Instead of nullable parameters that cause validation errors:
public IActionResult Search(string query, int? page = null, string? sortBy = null)
// Use Option<T> - missing values are None, not validation errors:
public IActionResult Search(string query, Option<int> page, Option<string> sortBy)
{
var actualPage = page.UnwrapOr(1);
var actualSort = sortBy.UnwrapOr("name");
// ...
}
🛡️ Production-Ready Error Handling
- RFC 7807 ProblemDetails format
- Automatic status code mapping
- Stack traces in development, clean responses in production
- Request correlation and tracing
- Type-safe error handling throughout your application
Installation
dotnet add package Esox.SharpAndRusty.AspNetCore
Quick Start
1. Configure Services
// Program.cs or Startup.cs
builder.Services.AddSharpAndRusty();
2. Add Middleware
// Development
if (app.Environment.IsDevelopment())
{
app.UseResultMiddlewareDevelopment(); // Includes stack traces
}
else
{
app.UseResultMiddlewareProduction(); // Production-safe
}
3. Use in Controllers
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
var result = _userService.GetUser(id); // Returns Result<User, Error>
return result.ToActionResult(); // Automatic conversion!
}
}
JSON Serialization
When returning functional types directly from API endpoints, they serialize cleanly to JSON:
Option<T> Serialization
[HttpGet("user/{id}")]
public Option<User> GetUser(int id)
{
return FindUser(id); // Option<User>
}
// Response when user exists:
// {
// "id": 123,
// "name": "John Doe",
// "email": "john@example.com"
// }
// Response when user doesn't exist:
// null
Result<T, E> Serialization
[HttpPost("user")]
public Result<User, Error> CreateUser([FromBody] CreateUserDto dto)
{
return ValidateAndCreateUser(dto); // Result<User, Error>
}
// Success response:
// {
// "id": 123,
// "name": "John Doe",
// "email": "john@example.com"
// }
// Error response: Throws JsonException (results should typically be converted to IActionResult)
ExtendedResult<T, E> Serialization
[HttpGet("user/{id}/details")]
public ExtendedResult<UserDetails, Error> GetUserDetails(int id)
{
return GetDetailedUserInfo(id); // ExtendedResult<UserDetails, Error>
}
// Success response:
// {
// "user": { "id": 123, "name": "John Doe" },
// "permissions": ["read", "write"],
// "lastLogin": "2023-12-01T10:30:00Z"
// }
// Error response: Throws JsonException (extended results should typically be converted to IActionResult)
Note: Result<T,E> and ExtendedResult<T,E> in error states cannot be serialized and will throw JsonException. For API responses, convert them to IActionResult using .ToActionResult() instead.
Action Result Conversions
Result<T, E> → IActionResult
// Automatic status code mapping for Result<T, Error>
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var result = GetUser(id); // Result<User, Error>
return result.ToActionResult(); // 200 OK or 404/400/500 based on ErrorKind
}
// Custom status code for generic errors
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var result = CreateUser(dto); // Result<User, string>
return result.ToActionResult(statusCode: 422); // 422 Unprocessable Entity on error
}
// Created result (201)
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var result = CreateUser(dto); // Result<User, Error>
return result.ToCreatedResult(user => $"/api/users/{user.Id}");
// Returns 201 Created with Location header
}
// No content result (204)
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var result = DeleteUser(id); // Result<Unit, Error>
return result.ToNoContentResult(); // 204 No Content or error
}
// Accepted result (202)
[HttpPost("batch")]
public IActionResult BatchProcess([FromBody] BatchRequest request)
{
var result = QueueBatchJob(request); // Result<JobId, Error>
return result.ToAcceptedResult($"/api/jobs/{result.Value.Id}");
// Returns 202 Accepted with Location
}
Option<T> → IActionResult
// Automatic 404 for None
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var option = FindUser(id); // Option<User>
return option.ToActionResult(); // 200 OK (Some) or 404 Not Found (None)
}
// Custom 404 message
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var option = FindUser(id); // Option<User>
return option.ToActionResult($"User {id} not found");
}
// Custom result mapping
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var option = FindUser(id); // Option<User>
return option.ToActionResult(
someResult: user => Ok(new { user, message = "Found!" }),
noneResult: () => NotFound(new { message = "Not found", id })
);
}
Either<L, R> → IActionResult
// Left = success, Right = error (Rust convention)
[HttpPost]
public IActionResult Process([FromBody] ProcessRequest request)
{
var either = ProcessData(request); // Either<ProcessResult, Error>
return either.ToActionResult(); // 200 OK (Left) or error (Right)
}
// Custom status code for Right
[HttpPost]
public IActionResult Process([FromBody] ProcessRequest request)
{
var either = ProcessData(request); // Either<ProcessResult, ValidationError>
return either.ToActionResult(rightStatusCode: 422);
}
Validation<T, E> → IActionResult
// Automatic ValidationProblemDetails format
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var validation = ValidateUser(dto); // Validation<User, Error>
return validation.ToActionResult();
// Returns 200 OK or 400 Bad Request with ALL validation errors
}
// Custom field mapping
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var validation = ValidateUser(dto); // Validation<User, FieldError>
return validation.ToValidationResult(
keySelector: error => error.FieldName,
messageSelector: error => error.Message
);
}
ExtendedResult<T, E> → IActionResult
ExtendedResult provides the same functionality as Result with additional context capabilities:
// Basic conversion with automatic status mapping
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var result = GetUserExtended(id); // ExtendedResult<User, Error>
return result.ToActionResult(); // 200 OK or appropriate error status
}
// Custom status code for generic errors
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var result = CreateUserExtended(dto); // ExtendedResult<User, string>
return result.ToActionResult(statusCode: 422); // 422 on error
}
// Created result (201)
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
var result = CreateUserExtended(dto); // ExtendedResult<User, Error>
return result.ToCreatedResult(user => $"/api/users/{user.Id}");
// Returns 201 Created with Location header
}
// No content result (204)
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var result = DeleteUserExtended(id); // ExtendedResult<Unit, Error>
return result.ToNoContentResult(); // 204 No Content or error
}
// Accepted result (202)
[HttpPost("batch")]
public IActionResult BatchProcess([FromBody] BatchRequest request)
{
var result = QueueBatchJobExtended(request); // ExtendedResult<JobId, Error>
return result.ToAcceptedResult($"/api/jobs/{result.Value.Id}");
// Returns 202 Accepted with Location
}
// Custom handlers for complete control
[HttpPost("process")]
public IActionResult Process([FromBody] ProcessRequest request)
{
var result = ProcessDataExtended(request); // ExtendedResult<ProcessResult, Error>
return result.ToActionResult(
onSuccess: data => Ok(new { data, processed = DateTime.UtcNow }),
onFailure: error => error.ToProblemDetails()
);
}
RFC 7807 ProblemDetails Integration
The library automatically converts Error types to RFC 7807 ProblemDetails with appropriate status codes:
Automatic Status Code Mapping
| ErrorKind | HTTP Status | Title |
|---|---|---|
NotFound |
404 | Not Found |
PermissionDenied |
403 | Forbidden |
Unauthorized |
401 | Unauthorized |
InvalidInput |
400 | Bad Request |
ParseError |
400 | Bad Request |
InvalidOperation |
400 | Bad Request |
AlreadyExists |
409 | Conflict |
Timeout |
408 | Request Timeout |
ResourceExhausted |
429 | Too Many Requests |
NotSupported |
501 | Not Implemented |
ConnectionRefused |
503 | Service Unavailable |
ConnectionReset |
503 | Service Unavailable |
Io |
500 | Internal Server Error |
Other |
500 | Internal Server Error |
Example ProblemDetails Response
// Code
var error = Error.New("User not found", ErrorKind.NotFound)
.WithContext("Database query returned no results")
.WithMetadata("userId", 123);
return error.ToProblemDetails();
// HTTP Response: 404 Not Found
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"detail": "User not found: Database query returned no results",
"instance": "/api/users/123",
"errors": [
{
"message": "User not found",
"kind": "NotFound"
},
{
"message": "Database query returned no results",
"kind": "NotFound"
}
],
"traceId": "0HM7...",
"requestId": "0HM7..."
}
Global Error Handling Middleware
Basic Setup
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSharpAndRusty();
var app = builder.Build();
// Add middleware based on environment
if (app.Environment.IsDevelopment())
{
app.UseResultMiddlewareDevelopment(); // Includes stack traces
}
else
{
app.UseResultMiddlewareProduction(); // Production-safe
}
app.MapControllers();
app.Run();
Custom Configuration
app.UseResultMiddleware(new ResultMiddlewareOptions
{
IncludeStackTrace = app.Environment.IsDevelopment(),
IncludeFileInfo = app.Environment.IsDevelopment(),
WriteIndented = app.Environment.IsDevelopment(),
// Only handle specific exceptions
HandleException = ex => ex is not OperationCanceledException,
// Add custom metadata
CustomMetadataProvider = (context, error) => error
.WithMetadata("user_id", context.User?.FindFirst("sub")?.Value)
.WithMetadata("tenant_id", context.Request.Headers["X-Tenant-Id"].FirstOrDefault())
});
What It Does
- Catches unhandled exceptions globally
- Converts to
Errortypes - Maps to ProblemDetails with appropriate status code
- Adds request context (trace ID, path)
- Logs errors using
ILogger - Returns JSON in RFC 7807 format
Model Binding
Option<T> Binding
The library provides automatic model binding for Option<T>, treating missing/null values as None instead of validation errors:
[HttpGet]
public IActionResult Search(
[FromQuery] string query,
[FromQuery] Option<int> pageSize, // Optional parameter
[FromQuery] Option<string> sortBy) // Optional parameter
{
var size = pageSize.UnwrapOr(10); // Default to 10
var sort = sortBy.UnwrapOr("name"); // Default to "name"
var results = _searchService.Search(query, size, sort);
return Ok(results);
}
// Usage:
// GET /api/search?query=test -> pageSize: None, sortBy: None
// GET /api/search?query=test&pageSize=20 -> pageSize: Some(20), sortBy: None
// GET /api/search?query=test&sortBy=date -> pageSize: None, sortBy: Some("date")
Complex Types
public class UpdateUserDto
{
public required string Name { get; set; }
public Option<string> Email { get; set; } // Optional update
public Option<DateTime> BirthDate { get; set; } // Optional update
public Option<string?> Bio { get; set; } // Optional, can be set to null
}
[HttpPatch("{id}")]
public IActionResult Update(int id, [FromBody] UpdateUserDto dto)
{
var user = GetUser(id);
// Only update fields that are Some
dto.Email.Iter(email => user.Email = email);
dto.BirthDate.Iter(date => user.BirthDate = date);
dto.Bio.Iter(bio => user.Bio = bio);
SaveUser(user);
return NoContent();
}
Supported Types
The OptionModelBinder supports all types that ASP.NET Core can bind:
- ✅ Primitive types:
int,string,bool,decimal,double,float,long, etc. - ✅ Date/Time types:
DateTime,DateTimeOffset,TimeSpan - ✅ Guid and other value types
- ✅ Enums: Both numeric and string-based
- ✅ Nullable types:
Option<int?>,Option<DateTime?>, etc. - ✅ Complex types: Classes, records, structs
- ✅ Collections:
List<T>,IEnumerable<T>, arrays - ✅ Nested Options:
Option<Option<T>>(though rarely needed)
How It Works
- Registration:
AddSharpAndRusty()registersOptionModelBinderProvider - Detection: Provider detects
Option<T>parameters/properties - Delegation: Creates
OptionModelBinderwith inner binder for typeT - Binding: Attempts to bind the inner value
- Success: Wraps value in
Some(value) - Failure/Missing: Returns
None
- Success: Wraps value in
- No Validation Errors: Unlike nullable types, missing
Option<T>values don't cause validation errors
Example with All Supported Types
public class SearchFiltersDto
{
// Primitives
public Option<int> Page { get; set; }
public Option<string> SortField { get; set; }
public Option<bool> IncludeArchived { get; set; }
public Option<decimal> MinPrice { get; set; }
// Date/Time
public Option<DateTime> CreatedAfter { get; set; }
public Option<DateTimeOffset> UpdatedBefore { get; set; }
// Guid
public Option<Guid> CategoryId { get; set; }
// Enums
public Option<ProductStatus> Status { get; set; }
// Nullable types
public Option<int?> Rating { get; set; } // Some(null) vs None
// Collections
public Option<List<string>> Tags { get; set; }
// Complex types
public Option<PriceRange> PriceRange { get; set; }
}
[HttpGet("products")]
public IActionResult SearchProducts([FromQuery] SearchFiltersDto filters)
{
var query = _db.Products.AsQueryable();
// Apply filters only if provided (Some)
filters.CategoryId.Iter(id => query = query.Where(p => p.CategoryId == id));
filters.Status.Iter(status => query = query.Where(p => p.Status == status));
filters.MinPrice.Iter(min => query = query.Where(p => p.Price >= min));
filters.CreatedAfter.Iter(date => query = query.Where(p => p.CreatedAt >= date));
filters.IncludeArchived.Iter(include => {
if (!include) query = query.Where(p => !p.IsArchived);
});
var results = query.ToList();
return Ok(results);
}
Real-World Examples
Example 1: CRUD Operations
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
[HttpGet]
public IActionResult GetAll()
{
var products = _productService.GetAll();
return Ok(products);
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
// Option<Product> - 200 OK or 404 Not Found
return _productService.FindById(id).ToActionResult();
}
[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto)
{
// Validation<Product, Error> - accumulates all validation errors
var validation = _productService.Validate(dto);
return validation
.Map(product => _productService.Create(product))
.ToActionResult(); // 200 OK with product or 400 with all errors
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] UpdateProductDto dto)
{
// Result<Product, Error> - short-circuits on first error
var result = _productService.Update(id, dto);
return result.ToActionResult(); // 200 OK or appropriate error status
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
// Result<Unit, Error>
return _productService.Delete(id).ToNoContentResult(); // 204 No Content or error
}
}
Example 2: Search with Optional Parameters
[ApiController]
[Route("api/[controller]")]
public class SearchController : ControllerBase
{
[HttpGet]
public IActionResult Search(
[FromQuery] string query,
[FromQuery] Option<int> page,
[FromQuery] Option<int> pageSize,
[FromQuery] Option<string> category,
[FromQuery] Option<decimal> minPrice,
[FromQuery] Option<decimal> maxPrice)
{
var searchParams = new SearchParameters
{
Query = query,
Page = page.UnwrapOr(1),
PageSize = pageSize.UnwrapOr(20),
Category = category,
PriceRange = minPrice.FlatMap(min =>
maxPrice.Map(max => new PriceRange(min, max)))
};
var results = _searchService.Search(searchParams);
return Ok(results);
}
}
Example 3: Complex Validation
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterDto dto)
{
// Validate all fields and accumulate errors
var validations = new[]
{
ValidateEmail(dto.Email),
ValidatePassword(dto.Password),
ValidateAge(dto.Age),
ValidateUsername(dto.Username)
};
// Sequence validations - returns Valid only if ALL are valid
var result = validations.Sequence() // From ValidationExtensions
.Map(values => new User(dto.Email, dto.Password, dto.Age, dto.Username))
.Bind(user => _userService.Create(user));
return result.ToActionResult();
// Returns all validation errors if any fail, or 200 OK with created user
}
private Validation<string, Error> ValidateEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return Validation<string, Error>.Invalid("Email is required");
if (!email.Contains("@"))
return Validation<string, Error>.Invalid("Email must be valid");
return Validation<string, Error>.Valid(email);
}
private Validation<string, Error> ValidatePassword(string password)
{
var errors = new List<Error>();
if (password.Length < 8)
errors.Add(Error.New("Password must be at least 8 characters"));
if (!password.Any(char.IsUpper))
errors.Add(Error.New("Password must contain uppercase letter"));
if (!password.Any(char.IsDigit))
errors.Add(Error.New("Password must contain number"));
return errors.Any()
? Validation<string, Error>.Invalid(errors)
: Validation<string, Error>.Valid(password);
}
}
Example 4: Async Operations with Result
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
{
// Chain async operations with Result
var result = await _orderService
.ValidateInventory(dto.Items)
.BindAsync(async _ => await _paymentService.ProcessPayment(dto.Payment))
.BindAsync(async payment => await _orderService.CreateOrder(dto, payment))
.MapAsync(async order => await _notificationService.SendConfirmation(order));
return result.ToCreatedResult(order => $"/api/orders/{order.Id}");
}
}
Integration with Existing Code
Wrapping Existing Services
public class UserService
{
// Existing method that might return null
public User? GetUserById(int id)
{
return _db.Users.Find(id);
}
// Wrap with Option
public Option<User> FindUserById(int id)
{
return Option<User>.From(GetUserById(id));
}
// Existing method that throws exceptions
public void DeleteUser(int id)
{
var user = GetUserById(id);
if (user == null)
throw new NotFoundException($"User {id} not found");
_db.Users.Remove(user);
_db.SaveChanges();
}
// Wrap with Result
public Result<Unit, Error> TryDeleteUser(int id)
{
try
{
DeleteUser(id);
return Result<Unit, Error>.Ok(Unit.Default);
}
catch (Exception ex)
{
return Result<Unit, Error>.Err(Error.FromException(ex));
}
}
}
Best Practices
✅ DO
// Use Result<T, Error> for operations that can fail
public Result<User, Error> GetUser(int id);
// Use Option<T> for values that may not exist
public Option<User> FindUser(int id);
// Use Validation<T, E> when you need ALL errors
public Validation<User, Error> ValidateUser(CreateUserDto dto);
// Let the middleware handle unhandled exceptions
app.UseResultMiddleware();
// Use appropriate status codes
return result.ToCreatedResult(user => $"/api/users/{user.Id}");
❌ DON'T
// Don't return nullable types when Option makes sense
public User? GetUser(int id); // ❌
public Option<User> GetUser(int id); // ✅
// Don't throw exceptions for expected failures
public User GetUser(int id)
{
var user = _db.Find(id);
if (user == null)
throw new NotFoundException(); // ❌
}
// Use Result instead ✅
public Result<User, Error> GetUser(int id);
// Don't catch and return 500 for everything
catch (Exception ex)
{
return StatusCode(500, "Error"); // ❌
}
// Let middleware handle it ✅
Configuration Options
Service Configuration
builder.Services.AddSharpAndRusty(options =>
{
options.EnableOptionModelBinding = true; // Enable Option<T> binding
options.EnableResultModelBinding = false; // Usually not needed
});
Middleware Configuration
app.UseResultMiddleware(new ResultMiddlewareOptions
{
// Include stack traces (dev only)
IncludeStackTrace = true,
// Include file paths in stack traces
IncludeFileInfo = true,
// Pretty-print JSON
WriteIndented = true,
// Filter exceptions
HandleException = ex => ex is not OperationCanceledException,
// Add custom metadata
CustomMetadataProvider = (context, error) =>
{
return error
.WithMetadata("correlation_id", context.TraceIdentifier)
.WithMetadata("user_agent", context.Request.Headers["User-Agent"].ToString());
}
});
Testing
The AspNetCore library currently reports 570 total test executions across .NET 8, 9, and 10 in the latest verified run:
Test Coverage
- ✅ Action Result Conversions (38 tests) - All conversion methods and edge cases
- ✅ OptionModelBinder (69 tests) - Comprehensive model binding scenarios
- ✅ OptionModelBinderProvider (66 tests) - Provider registration and type detection
Example Tests
[Fact]
public void GetUser_WithValidId_ReturnsOk()
{
// Arrange
var user = new User { Id = 1, Name = "Test" };
_mockService.Setup(s => s.GetUser(1))
.Returns(Result<User, Error>.Ok(user));
// Act
var result = _controller.GetUser(1);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
Assert.Equal(user, okResult.Value);
}
[Fact]
public void GetUser_WithInvalidId_ReturnsNotFound()
{
// Arrange
var error = Error.New("Not found", ErrorKind.NotFound);
_mockService.Setup(s => s.GetUser(999))
.Returns(Result<User, Error>.Err(error));
// Act
var result = _controller.GetUser(999);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(404, objectResult.StatusCode);
}
[Fact]
public void OptionModelBinder_WithProvidedValue_BindsToSome()
{
// Arrange
var binder = CreateOptionModelBinder<int>();
var context = CreateBindingContext<int>(42);
// Act
await binder.BindModelAsync(context);
// Assert
Assert.True(context.Result.IsModelSet);
var option = Assert.IsType<Option<int>.Some>(context.Result.Model);
Assert.Equal(42, option.Value);
}
[Fact]
public void OptionModelBinder_WithMissingValue_BindsToNone()
{
// Arrange
var binder = CreateOptionModelBinder<int>();
var context = CreateBindingContextWithoutValue<int>();
// Act
await binder.BindModelAsync(context);
// Assert
Assert.True(context.Result.IsModelSet);
Assert.IsType<Option<int>.None>(context.Result.Model);
}
For complete test coverage details, see TEST_DOCUMENTATION.md.
Recent Updates
✅ Security Update (Latest)
Fixed: Vulnerable Microsoft.AspNetCore package dependencies (CVE-2018-8269 and others)
Action Taken:
- Removed deprecated Microsoft.AspNetCore.* packages (version 2.3.9 from 2018)
- Migrated to
FrameworkReferencefor Microsoft.AspNetCore.App - All packages now use secure, framework-provided versions
Verification:
dotnet list package --vulnerable --include-transitive
# Result: No vulnerable packages found ✅
This change provides:
- ✅ Security: No known vulnerabilities
- ✅ Compatibility: Works with .NET 8, 9, and 10
- ✅ Maintenance: Framework-managed versions
- ✅ Performance: Latest optimizations
Project Status
| Metric | Status |
|---|---|
| Build | ✅ Passing |
| Tests | ✅ 570/570 passing |
| Vulnerabilities | ✅ 0 found |
| Target Frameworks | .NET 8.0, 9.0, 10.0 |
| Code Coverage | 100% |
| Documentation | Complete |
Contributing
Contributions are welcome! Please follow these guidelines:
Getting Started
# Clone the repository
git clone https://github.com/snoekiede/Esox.SharpAndRusty.AspNetCore.git
cd Esox.SharpAndRusty.AspNetCore
# Restore dependencies
dotnet restore
# Build
dotnet build
# Run tests
dotnet test
Development Guidelines
- Write tests first - Follow TDD principles
- Maintain 100% coverage - All public APIs must be tested
- Follow existing patterns - Use the AAA pattern (Arrange-Act-Assert)
- Test all frameworks - Ensure compatibility with .NET 8, 9, and 10
- Update documentation - Keep README and docs in sync with code changes
Pull Request Process
- Create a feature branch (
feature/your-feature-name) - Write tests for your changes
- Ensure all tests pass (
dotnet test) - Update documentation as needed
- Submit a pull request with a clear description
Code Style
- Use C# 12 features where appropriate
- Enable nullable reference types
- Follow standard C# conventions
- Keep methods focused and single-purpose
- Add XML documentation comments for public APIs
Documentation
- README.md - This file, comprehensive library documentation
- COMPILATION_FIXES.md - Build status and troubleshooting
- TEST_DOCUMENTATION.md - Complete test suite documentation
Dependencies
Production
- Esox.SharpAndRusty (1.5.1) - Core functional types library
- Microsoft.AspNetCore.App (Framework) - ASP.NET Core shared framework
- Microsoft.Extensions.Logging (10.0.5) - Logging abstractions
Development/Testing
- xUnit (2.9.3) - Testing framework
- xunit.runner.visualstudio (3.1.4) - Visual Studio test adapter
- Microsoft.NET.Test.Sdk (17.14.1) - .NET test SDK
- Moq (4.20.72) - Mocking framework
- coverlet.collector (6.0.4) - Code coverage collection
Versioning
This project uses Semantic Versioning:
- MAJOR version for incompatible API changes
- MINOR version for new functionality in a backward compatible manner
- PATCH version for backward compatible bug fixes
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Core Library: Esox.SharpAndRusty
Acknowledgments
Built with ❤️ using:
- Esox.SharpAndRusty - Functional programming for C#
- ASP.NET Core - Web framework
- xUnit - Testing framework
Made with functional programming principles and type safety in mind.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Esox.SharpAndRusty (>= 1.6.0)
-
net8.0
- Esox.SharpAndRusty (>= 1.6.0)
- Microsoft.Extensions.Logging (>= 10.0.5)
-
net9.0
- Esox.SharpAndRusty (>= 1.6.0)
- Microsoft.Extensions.Logging (>= 10.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.