CustomResultError 1.4.1

dotnet add package CustomResultError --version 1.4.1
                    
NuGet\Install-Package CustomResultError -Version 1.4.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CustomResultError" Version="1.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CustomResultError" Version="1.4.1" />
                    
Directory.Packages.props
<PackageReference Include="CustomResultError" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CustomResultError --version 1.4.1
                    
#r "nuget: CustomResultError, 1.4.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CustomResultError@1.4.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CustomResultError&version=1.4.1
                    
Install as a Cake Addin
#tool nuget:?package=CustomResultError&version=1.4.1
                    
Install as a Cake Tool

CustomResultError

The modern Result/Error types adjusted to modern AOT needs.

Combine the power of Results and Errors using a "discriminated union" approach.

How to install

Via the Package Manager:

Install-Package CustomResultError

Via the .NET CLI:

dotnet add package CustomResultError

Error

Generic Error<CodeType> inherits from the Error base class. The Error class is an immutable object which contains the properties Message (string) and Details (string[]). For the generic error, CodeType is the type of an additional Code property. Errors are considered equal if their corresponding Code properties are equal.

Error<int> e4 = new("mpe", code: 125);
Error<int> e5 = new("mpou", code: 125);
Console.WriteLine(e5);       // prints "mpou"
Console.WriteLine(e4 == e5); // prints "True" because their codes are the same

Error<string> e6 = new("mpa", "CODE1");
Error<string> e7 = new("mpampou", "CODE1");
Console.WriteLine(e6 == e7); // prints "True" because their codes are the same

In the AOT world you cannot use JsonSerializer methods because they rely on Reflection. For this reason, Error objects expose a ToJsonString() method which simplifies output especially in the case of Web endpoints. The ToString() overridden method returns only the Message. Below are examples of how to produce a full JSON string:

// simplest case
Error<int> e4 = new("mpe", 125);
Console.WriteLine(e4.ToJsonString());

// with sub-errors/details (pass extra strings or a string array)
Error<int> e4a = new("mpe", 125, "suberror1", "suberror2");
Console.WriteLine(e4a.ToJsonString());

The first case prints:

{
    "code": 125,
    "message": "mpe"
}

The second case prints:

{
    "code": 125,
    "message": "mpe",
    "details": ["suberror1", "suberror2"]
}

ExceptionError

ExceptionError is a special Error that inherits from Error<Exception>. For convenience it prepends an optional domain (string) to the exception type name to form a domain-specific error code. The Message property is taken from Exception.Message, and the Details array is populated from inner exceptions if they exist. The Code property holds the full Exception object, so stack information is preserved.

Exception e = new InvalidOperationException("bamboo", new OperationCanceledException("mpeeee"));
ExceptionError error = new(e, domain: "MAIN");
Console.WriteLine(error.ToJsonString());

Output:

{
    "code": "MAIN.InvalidOperationException",
    "message": "bamboo",
    "details": ["mpeeee"]
}

Result

The Result class is designed to behave like a discriminated union. A simple example below shows implicit conversion from a value or an error to a Result instance:

Result<int, Error<string>> result;

int a = 5, b = 6;
if (a < b)
    result = Result.Ok(a + b);
else
    result = Result.Fail(new Error<string>("This was a bad calc.", "App.CalcError"));

// equivalent, using implicit conversions
if (a < b)
    result = a + b;
else
    result = new Error<string>("This was a bad calc.", "App.CalcError");

The Result instance exposes Value and Error properties:

IResult res;
if (result.IsSuccess)
    res = Results.Ok(result.Value);
else
    res = Results.BadRequest(result.Error);

There are more compact ways to express the same logic using the Match function:

res = result.Match<IResult>(v => Results.Ok(v), e => Results.BadRequest(e));

// or using method groups
res = result.Match<IResult>(Results.Ok, Results.BadRequest);

// or with inferred return type
res = result.Match(Results.Ok, Results.BadRequest);

Match takes two Func arguments: the first maps the Value to a result, the second maps the Error to a result.

The Switch function is similar to Match but takes Action arguments (no return value). The first Action is called on success, the second on failure:

result.Switch(
    v => Console.WriteLine($"YES! The value is {v}"),
    e => Console.WriteLine($"NO! The error is {e}"));

Error parsing and AOT

The problem with AOT is that Reflection is not supported. Methods such as AsJsonAsync will not work. The Error class supports parsing without Reflection and is therefore AOT-compatible.

The examples below show round-trip serialization and parsing. The jsonString might come from the text response content of an HTTP call:

Error<int> e1 = new("messsad", 200, "sub1", "sub2");
string jsonString = e1.ToJsonString();
var e1_c = Error<int>.Parse(jsonString); // no Reflection used
Console.WriteLine(e1 == e1_c); // prints "True"

Error<string> e2 = new(message: "messsad", code: "DSAD.asd", "sub1", "sub2");
jsonString = e2.ToJsonString();
var e2_c = Error<string>.Parse(jsonString); // no Reflection used
Console.WriteLine(e2 == e2_c); // prints "True"

The Validator static class

The CustomResultError.Validator static class combines validation, logging and the Error type in one place. Other validation libraries (e.g. FluentValidation) rely heavily on Reflection and cannot be used in AOT apps — Validator can.

It is practical to import the static members globally:

using static CustomResultError.Validator;

// convenient alias for string-coded errors
using ErrorString = CustomResultError.Error<string>;

Validate

Validate is a generic function that returns an Error only when validation fails. If an ILogger is supplied, the error message template is logged using structured logging. The same message becomes the Message property of the returned error. null is returned on success; no logging is done on success to avoid verbose output.

static ErrorString? ValidateTaxValue(int valueInPerc, ILogger? logger)
{
    return Validate(
        value: valueInPerc,
        validateFunction: p => p >= 0 && p <= 100,
        logger: logger,
        errorCode: "App.InvalidTax",
        errorMessageTemplate: "The tax value ({value}) is invalid.",
        logTypeIfError: ValidatorLogTypeIfError.Error,
        messageArgs: valueInPerc);
}

Fail

Fail returns a non-nullable Error and logs the message to the ILogger (if given). The method below is equivalent to the Validate example above:

static ErrorString? ValidateTaxValue(int valueInPerc, ILogger? logger)
{
    if (valueInPerc < 0 || valueInPerc > 100)
        return Fail(
            logger: logger,
            errorCode: "App.InvalidTax",
            errorMessageTemplate: "The tax value ({value}) is invalid.",
            logTypeIfError: ValidatorLogTypeIfError.Error,
            messageArgs: valueInPerc);

    return null;
}

Chaining multiple validations

A common pattern is to chain validations and return on the first failure:

static ErrorString? ValidateOrder(int quantity, decimal price, ILogger? logger)
{
    return
        Validate(quantity, q => q > 0, logger, "App.InvalidQuantity",
            "The quantity ({value}) must be positive.", ValidatorLogTypeIfError.Warning, quantity) ??
        Validate(price, p => p >= 0, logger, "App.InvalidPrice",
            "The price ({value}) cannot be negative.", ValidatorLogTypeIfError.Warning, price);
}

Because Validate returns null on success, the ?? operator chains validations and short-circuits on the first error.


The FileDependencies namespace

CustomResultError.FileDependencies provides an AOT-friendly system for validating JSON configuration files that reference external files on disk. It integrates directly with Result and Error so all validation failures are surfaced as typed errors.

Core types

Type Description
SingleFile Holds a FileName (relative) and FullPath (absolute) for a single file on disk.
Dependency Abstract base with Name, IsOptional and IsEmpty.
SingleFileDependency A named Dependency wrapping a SingleFile.
MultipleFilesDependency A named Dependency wrapping a List<SingleFile>, implements IEnumerable<SingleFile>.
Dependencies Composite container holding lists of SingleFileDependency, MultipleFilesDependency, and nested Dependencies.
FileValidator Abstract base class — inherit this to build a validator for your own JSON config format.

FileValidator

Inherit FileValidator and implement the single abstract method Validate(string filePath, JsonDocument jsonDocument). The public entry point Validate(string? filePath) handles reading, JSON-parsing and common error cases for you, then delegates to your implementation.

using CustomResultError.FileDependencies;

public class AppConfigValidator : FileValidator
{
    public AppConfigValidator(ILogger logger) : base(logger) { }

    protected override Result<Dependencies, ErrorString> Validate(string filePath, JsonDocument json)
    {
        // resolve a required single-file field: { "model": "weights/model.bin" }
        var modelResult = CheckFileFieldProperty(json.RootElement, "model", isOptional: false, filePath);
        if (modelResult.IsFailure) return modelResult.Error!;

        // resolve a required array of files: { "inputs": ["a.csv", "b.csv"] }
        var inputsResult = CheckFilesFieldProperty(json.RootElement, "inputs", isOptional: false, filePath);
        if (inputsResult.IsFailure) return inputsResult.Error!;

        return new Dependencies
        {
            Name = filePath,
            SingleFiles = [modelResult.Value!],
            MultipleFiles = [inputsResult.Value!]
        };
    }
}

Using the validator:

var validator = new AppConfigValidator(logger);
var result = validator.Validate("config.json");

result.Switch(
    deps =>
    {
        string modelPath = deps.GetSingleFileDependency("model")!.FullPath!;
        foreach (SingleFile input in deps.GetMultipleFilesDependency("inputs")!)
            Console.WriteLine(input.FullPath);
    },
    error => Console.WriteLine($"Config error: {error.ToJsonString()}"));

Use /-separated paths to reach nested properties without manual traversal:

// JSON: { "training": { "data": { "labels": "labels.csv" } } }
var labelsResult = CheckFileFieldProperty(json, "training/data/labels", isOptional: false, filePath);

Resolving an array of objects with a file field

When the config contains an array of objects each with a file field:

// JSON: { "stages": [ { "script": "step1.py" }, { "script": "step2.py" } ] }
var stagesResult = CheckPropertyFilesFieldProperty(json.RootElement, "stages", "script", isOptional: false, filePath);
if (stagesResult.IsFailure) return stagesResult.Error!;

List<SingleFileDependency> scripts = stagesResult.Value!;
foreach (var dep in scripts)
    Console.WriteLine($"{dep.Name} -> {dep.FullPath}");

Probing for missing files without errors

Use the helper methods when you want to report missing files without failing the whole validation:

// returns the path string if the file is missing, null if it exists or the property is absent
string? missing = GetMissingFileOrEmpty(json, "optional/asset", filePath);
if (missing is not null)
    Console.WriteLine($"Warning: '{missing}' not found.");

// returns all missing paths from an array of objects
List<string> missingInputs = GetMissingFilesFromArray(json, "inputs", "path", filePath);
foreach (var m in missingInputs)
    Console.WriteLine($"Missing input: {m}");

Relative and absolute paths

All file resolution is handled automatically by GetFullPath: relative paths in the JSON are resolved relative to the JSON file's own directory, while absolute paths are used as-is. This means configs are portable regardless of the working directory.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CustomResultError:

Package Downloads
ParserLibrary

Parser Library that can be customized for multiple data types (double, complex, vector, matrices, chords or whatever you want). Functions can be defined with multiple arguments, postfix and prefix multiple operators etc. Documentation and examples are being added currently. See README.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.1 125 7/15/2026
1.4.0 1,903 11/15/2025
1.3.0 465 10/13/2025
1.2.16 332 4/27/2025
1.2.15 482 12/6/2024
1.2.14 323 7/20/2024
1.2.13 198 7/14/2024
1.2.12 229 5/4/2024
1.2.11 212 5/3/2024
1.2.10 196 5/3/2024
1.2.9 199 5/3/2024
1.2.8 192 5/3/2024
1.2.7 187 5/3/2024
1.2.6 188 5/3/2024
1.2.5 178 5/3/2024
1.2.4 182 5/3/2024
1.2.2 170 5/3/2024
1.2.1 174 5/3/2024
1.2.0 173 5/3/2024
1.1.15 233 3/28/2024
Loading failed