Portamical.Core
1.0.0
See the version list below for details.
dotnet add package Portamical.Core --version 1.0.0
NuGet\Install-Package Portamical.Core -Version 1.0.0
<PackageReference Include="Portamical.Core" Version="1.0.0" />
<PackageVersion Include="Portamical.Core" Version="1.0.0" />
<PackageReference Include="Portamical.Core" />
paket add Portamical.Core --version 1.0.0
#r "nuget: Portamical.Core, 1.0.0"
#:package Portamical.Core@1.0.0
#addin nuget:?package=Portamical.Core&version=1.0.0
#tool nuget:?package=Portamical.Core&version=1.0.0
Portamical
A Universal, Identity-Driven Test Data Modeling Framework for .NET
Write test data once. Run it on xUnit v2, xUnit v3, MSTest 4, and NUnit 4βwithout rewriting tests or losing strong typing.
Portamical is the test data abstraction layer missing from the .NET testing ecosystem. It treats test data as first-class domain objects with deterministic identity, enabling automatic deduplication, cross-framework portability, and self-documenting test output.
The Problem It Solves
| Traditional Approach | Portamical Approach |
|---|---|
| π΄ Duplicate test data per framework | β Write once, consume everywhere |
π΄ Fragile object[] arrays |
β Strongly typed generics (up to 9 args) |
| π΄ Cryptic test names in runners | β Human-readable "definition β result" |
| π΄ Duplicate test cases slip through | β Built-in identity-based deduplication |
| π΄ Exception assertions differ by framework | β
Unified PortamicalAssert with delegate injection |
| π΄ Boilerplate test data setup | β
TestDataFactory with fluent creation |
| π΄ Mutable test state | β
init-only properties throughout |
Quick Start
1. Clone and Build
git clone https://github.com/CsabaDu/Portamical.git
cd Portamical
dotnet build
dotnet test
2. Choose Your Framework Solution
| Framework | Solution File |
|---|---|
| Core Library | Portamical.Core.slnx |
| Shared Layer | Portamical.slnx |
| xUnit v2 | Portamical.xUnit.slnx |
| xUnit v3 | Portamical.xUnit_v3.slnx |
| MSTest 4 | Portamical.MSTest.slnx |
| NUnit 4 | Portamical.NUnit.slnx |
3. Create Your First Data Source
using static Portamical.Core.Factories.TestDataFactory;
// Identityβdriven test cases with deterministic naming
public class EmailValidationCases
{
public IEnumerable<TestData<string>> GetValidArgs()
{
// Each test case defines:
// - a humanβreadable identity ("definition")
// - the expected outcome ("result")
// - the argument sequence (arg1, arg2, ...)
yield return CreateTestData(
definition: "input is a valid email",
result: "validates successfully",
arg1: "user@example.com");
yield return CreateTestData(
definition = "input is a valid name",
result = "validates successfully",
arg1 = "John Doe");
}
}
Power users can combine specialized testβdata types with local helper functions to keep factory calls consistent, expressive, and invariantβsafe:
public class AdvancedEmailValidationCases
{
public IEnumerable<TestData<string>> ValidEmails()
{
// Local helper:
// - Centralizes the factory call shape
// - Ensures argument ordering stays invariant across all yields
// - Makes edits safer: update the variables once, keep call sites unchanged
TestDataReturns<bool, string> createTestData()
=> CreateTestDataReturns(
definition: definition,
expected: expected,
arg1: email);
// First case
string definition = "input is a valid email";
bool expected = true;
string email = "user@example.com";
yield return createTestData();
// Reassign the same variables for the next identity
definition = "input is a valid email with subdomain";
expected = true;
email = "john.doe@mail.example.com";
yield return createTestData();
}
}
4. Consume Across All Frameworks
// xUnit v2/v3
[Theory, MemberData(nameof(Args))]
public void Validate_validInput_returnsTrue(TestData<string> testData)
{
var actual = Validate(testData.Arg1);
Assert.True(actual);
}
// MSTest
[TestMethod, DynamicData(nameof(Args))]
public void Validate_validInput_returnsTrue(TestData<string> testData)
{
var actual = Validate(testData.Arg1);
Assert.IsTrue(actual);
}
// NUnit
[Test, TestCaseSource(nameof(Args))]
public void Validate_validInput_returnsTrue(TestData<string> testData)
{
var actual = Validate(testData.Arg1);
Assert.That(actual, Is.True);
}
Architecture
Layered Design (Zero-Dependency Core)
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β _SampleCodes β β Reference implementations
β (Testables, DataSources, UnitTests) β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββΌβββββββββββββββββββββββββ
β Portamical.xUnit | xUnit_v3 | MSTest | NUnit β β Framework adapters
β (Thin adapter layer) β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββΌβββββββββββββββββββββββββ
β Portamical β β Shared utilities
β (Converters, Assertions, TestBases) β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
β depends on
βββββββββββββββββββββββββΌβββββββββββββββββββββββββ
β Portamical.Core β β Pure abstractions
β (Interfaces, Models, Factory β ZERO DEPS) β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Namespace Dependency Diagram:
Class Hierarchy (Template Method + Composite)
NamedCase (abstract) : INamedCase : IEquatable<INamedCase>
βββ TestDataBase (abstract) : ITestData
βββ TestData<T1..T9> (abstract)
β βββ [T4-generated: TestData<T1> β ... β TestData<T1,...,T9>]
βββ TestDataExpected<TResult> (abstract) : IExpected<TResult>
βββ TestDataReturns<TStruct> : IReturns<TStruct>
β βββ [T4-generated: TestDataReturns<TStruct,T1> β ... β <TStruct,T1,...,T9>]
βββ TestDataThrows<TException> : IThrows<TException>
βββ [T4-generated: TestDataThrows<TException,T1> β ... β <TException,T1,...,T9>]
Key: T4 code generation eliminates 27 classes worth of boilerplate while maintaining type safety.
Core Innovation: Identity-Driven Test Cases
Every test case is an immutable value object with deterministic identity:
"<definition> => <result>"
Real Examples from the Codebase
| Test Case Identity |
|---|
"Valid name and dateOfBirth is equal with the current day => creates BirthDay instance" |
"name is null => throws ArgumentNullException" |
"other is null => returns -1" |
What This Enables
Automatic Deduplication
// Built into TheoryTestData<TTestData> private readonly HashSet<INamedCase> _namedCases = new(NamedCase.Comparer); public override void Add(ITheoryTestDataRow row) { if (_namedCases.Add(row)) // β Identity-based add { base.Add(row); } }Self-Documenting Test Output
- Test runners display:
"input is valid email => validates successfully" - No manual
[Trait],[TestName], orDisplayNameattributes needed
- Test runners display:
Cross-Framework Consistency
- Same identity regardless of xUnit, MSTest, or NUnit
- Enables traceability from requirement β test data β execution
Value-Based Equality
public static IEqualityComparer<INamedCase> Comparer { get; } = new NamedCaseEqualityComparer(); public bool Equals(INamedCase? x, INamedCase? y) => StringComparer.Ordinal.Equals(x.TestCaseName, y.TestCaseName);
Test Data Types
Universal Test Data Model
| Type | Purpose | Constraints | Use Case |
|---|---|---|---|
TestData<T1..T9> |
General scenarios | Up to 9 arguments | Constructor tests, basic behavior |
TestDataReturns<TStruct, T1..T9> |
Return-value assertions | TStruct : struct |
Method output validation |
TestDataThrows<TException, T1..T9> |
Exception assertions | TException : Exception |
Error handling tests |
Factory Pattern (T4-Generated)
using static Portamical.Core.Factories.TestDataFactory;
// General test data
yield return CreateTestData(
definition: "Valid input",
result: "creates instance",
arg1: "valid name",
arg2: DateOnly.FromDateTime(DateTime.Now));
// Return-value test data
yield return CreateTestDataReturns(
definition: "other is null",
expected: -1, // β TStruct (value type)
arg1: dateOfBirth,
arg2: (BirthDay?)null);
// Exception test data
yield return CreateTestDataThrows(
definition: "name is null",
expected: new ArgumentNullException("name"),
arg1: (string?)null);
Key: Factory methods are T4-generated from a single source (SharedHelpers.ttinclude). Change MaxArity = 9 to support more arguments, regenerate, and build.
Data Strategy Pattern
The Strategy Pattern (ArgsCode + PropsCode) controls how test data materializes into framework-consumable rows.
Strategy Modes
1. TestData Mode (Direct Instance Flow)
// Data source
public static IEnumerable<TTestData> Data => Convert(dataSource.GetArgs());
// Test signature
void Test(TestData<DateOnly> testData) { ... }
2. Instance Mode (ArgsCode.Instance)
// Data source
public static IEnumerable<object?[]> Data => Convert(dataSource.GetArgs());
// Test signature (same as TestData mode)
void Test(TestData<DateOnly> testData) { ... }
3. Properties Mode (ArgsCode.Properties)
// Data source
public static IEnumerable<object?[]> Data
=> Convert(dataSource.GetArgs(), AsProperties);
// Test signature (flattened parameters)
void Test(DateOnly dateOfBirth) { ... }
PropsCode Options
PropsCode |
Includes | Use Case |
|---|---|---|
All |
TestCaseName + all properties |
MSTest with DynamicDataDisplayName |
TrimTestCaseName |
All properties except TestCaseName |
Default β test runner provides naming |
TrimReturnsExpected |
Also excludes Expected if IReturns |
NUnit return-value tests |
TrimThrowsExpected |
Also excludes Expected if IThrows |
Exception tests where assertion extracts exception |
Design Patterns Catalog
Portamical implements 15 GoF and architectural patterns to achieve portability and maintainability.
| Pattern | Implementation | Purpose |
|---|---|---|
| Factory | TestDataFactory (T4-generated) |
Centralized test data creation |
| Builder | Named parameters in factory methods | Fluent, self-documenting API |
| Adapter | Framework-specific adapters | Translate ITestData to framework types |
| Strategy | ArgsCode + PropsCode enums |
Configurable data serialization |
| Template Method | TestDataBase.ToArgs() |
Skeleton algorithm with hooks |
| Composite | Test data hierarchy | Uniform treatment of test data |
| Command | Delegate injection in PortamicalAssert |
Framework-agnostic assertions |
| Iterator | IEnumerable<TTestData> |
Lazy test data evaluation |
| Null Object | Optional testMethodName |
Eliminate null checks |
| Identity | NamedCase.Comparer |
Value object with deterministic identity |
| Dependency Inversion | Layered architecture | Core has zero dependencies |
| Repository | Data sources | Centralized test data storage |
| Code Generation | T4 templates | Single source of truth (MaxArity) |
| Local Method Pattern | static local functions |
Encapsulated helpers, zero closure cost |
| Bridge | Core β Adapters | Decouple abstraction from implementation |
T4 Code Generation
How It Works
All generic test data classes and the factory are generated at design time by T4 templates.
Portamical.Core/
βββ T4/
β βββ SharedHelpers.ttinclude β Single source of truth (MaxArity = 9)
βββ Factories/
β βββ TestDataFactory.tt β Template
β βββ TestDataFactory.generated.cs β Auto-generated output (736 lines)
βββ TestDataTypes/Models/
βββ General/
β βββ TestData.tt β Template
β βββ TestData.generated.cs β Auto-generated output
βββ Specialized/
βββ TestDataReturns.tt β Template
βββ TestDataReturns.generated.cs
βββ TestDataThrows.tt β Template
βββ TestDataThrows.generated.cs
Centralized Configuration
All four templates share a single SharedHelpers.ttinclude file:
// Portamical.Core/T4/SharedHelpers.ttinclude
const int MaxArity = 9; // β Change once, regenerate all
Regeneration Process
# 1. Edit SharedHelpers.ttinclude β change MaxArity
vim Portamical.Core/T4/SharedHelpers.ttinclude
# 2. In Visual Studio: select all 4 .tt files
# 3. Right-click β Run Custom Tool
# 4. Build
dotnet build
Design Decisions
| Template | Pattern | Include Placement | Rationale |
|---|---|---|---|
TestData.tt |
Inline text output | End of file | Extra trailing newline is harmless |
TestDataReturns.tt |
Inline text output | End of file | Same |
TestDataThrows.tt |
Inline text output | End of file | Same |
TestDataFactory.tt |
StringBuilder |
Joined before line 5 | Prevents CS8802 (second compilation unit) |
Note: The .generated.cs files are checked into source control so the solution builds without running T4 transformations.
Framework Adapters
Thin, optional adapters bridge Portamical to each test runner:
| Project | Framework | Key Integration |
|---|---|---|
Portamical.xUnit |
xUnit v2 | TheoryData<T> via ToTheoryData() |
Portamical.xUnit_v3 |
xUnit v3 (3.2.2+) | MemberTestDataAttribute, ITheoryTestDataRow |
Portamical.MSTest |
MSTest 4 (4.0.2+) | DynamicTestDataAttribute |
Portamical.NUnit |
NUnit 4 (4.4.0+) | TestCaseDataSourceAttribute, TestCaseTestData |
Same Data Source, Four Frameworks
// Shared β works everywhere
private static readonly BirthDayDataSource _dataSource = new();
// xUnit v2 / v3
public static IEnumerable<object?[]> Args => Convert(_dataSource.GetConstructorValidArgs());
[Theory, MemberData(nameof(Args))]
// MSTest
private static IEnumerable<object?[]> Args => Convert(_dataSource.GetConstructorValidArgs());
[TestMethod, DynamicData(nameof(Args))]
// NUnit
public static IEnumerable<object?[]> Args => Convert(_dataSource.GetConstructorValidArgs(), AsInstance);
[Test, TestCaseSource(nameof(Args))]
Unified Exception Assertions
PortamicalAssert.ThrowsDetails validates exception type, message, and parameter name using delegate injection (Command Pattern):
// xUnit
PortamicalAssert.ThrowsDetails(attempt, expected,
catchException: Record.Exception,
assertIsType: Assert.IsType,
assertEquality: Assert.Equal,
assertFail: Assert.Fail);
// NUnit
PortamicalAssert.ThrowsDetails(attempt, expected,
catchException: CatchException,
assertIsType: (e, a) => Assert.That(a, Is.TypeOf(e)),
assertEquality: (e, a) => Assert.That(a, Is.EqualTo(e)),
assertFail: Assert.Fail);
// MSTest
PortamicalAssert.ThrowsDetails(attempt, expected,
catchException: CatchException,
assertIsType: (e, a) => Assert.AreEqual(e, a.GetType()),
assertEquality: (e, a) => Assert.AreEqual(e, a),
assertFail: Assert.Fail);
Key: Framework-specific assertion methods are injected as delegates, making the core logic framework-agnostic.
Sample Code Walkthrough
The _SampleCodes folder contains a complete BirthDay class example:
The Testable Class
// _SampleCodes/Testables/SampleClasses/BirthDay.cs
public class BirthDay : IComparable<BirthDay>
{
public string Name { get; init; }
public DateOnly DateOfBirth { get; init; }
public BirthDay(string name, DateOnly dateOfBirth) { ... }
public int CompareTo(BirthDay? other) => ...;
}
The Data Source (Framework-Agnostic)
// _SampleCodes/DataSources/TestDataSources/BirthDayDataSource.cs
public class BirthDayDataSource
{
// TestData<DateOnly> β general constructor scenarios
public IEnumerable<TestData<DateOnly>> GetBirthDayConstructorValidArgs()
{
const string result = "creates BirthDay instance";
string definition = "Valid name and dateOfBirth is equal with the current day";
DateOnly dateOfBirth = Today;
yield return createTestData(); // β Local method pattern
#region Local Methods
TestData<DateOnly> createTestData()
=> CreateTestData(definition, result, dateOfBirth);
#endregion
}
// TestDataThrows<ArgumentException, string> β exception scenarios
public IEnumerable<TestDataThrows<ArgumentException, string>> GetBirthDayConstructorInvalidArgs() { ... }
// TestDataReturns<int, DateOnly, BirthDay> β return-value scenarios
public IEnumerable<TestDataReturns<int, DateOnly, BirthDay>> GetCompareToArgs() { ... }
}
Test Classes (One Data Source β Four Frameworks)
| Framework | Instance Mode | Properties Mode |
|---|---|---|
| xUnit v2 | _UnitTests/xUnit/ |
_UnitTests/xUnit/ |
| xUnit v3 | _UnitTests/xUnit_v3/ |
_UnitTests/xUnit_v3/Specific/ |
| MSTest 4 | _UnitTests/MSTest/Native/..._Instance.cs |
_UnitTests/MSTest/Native/..._Properties.cs |
| NUnit 4 | _UnitTests/NUnit/Native/..._Instance.cs |
_UnitTests/NUnit/Native/..._Properties.cs |
# Run the MSTest sample
dotnet test _SampleCodes/_UnitTests/MSTest/
# Run the NUnit sample
dotnet test _SampleCodes/_UnitTests/NUnit/
# Run the xUnit v3 sample
dotnet test _SampleCodes/_UnitTests/xUnit_v3/
Prerequisites
- .NET 10 SDK (Preview or later)
- Visual Studio 2022 17.14+ with Text Template Transformation component (for T4 regeneration)
- One or more test frameworks:
- xUnit v2 (
xunit2.x) - xUnit v3 (
xunit.v33.2.2+) - MSTest 4 (
MSTest.TestFramework4.0.2+) - NUnit 4 (
NUnit4.4.0+)
- xUnit v2 (
Contributing
Contributions are welcome! Here's how to get started:
1. Fork and Branch
git checkout -b feature/my-improvement
2. Follow Code Conventions
- SPDX license headers on all
.csfiles init-only properties for immutability- XML doc comments on public APIs
- Local methods with
#region Local methodsfor encapsulation - Naming:
camelCasefor local methods,PascalCasefor public APIs
3. Regenerate T4 Output (If Applicable)
If you modified any .tt or .ttinclude files:
# In Visual Studio:
# Right-click the .tt files β Run Custom Tool
# Commit the updated .generated.cs files
4. Build and Test
dotnet build
dotnet test
5. Open a Pull Request
Submit against master with a clear description.
Branch Conventions
| Branch | Purpose |
|---|---|
master |
Stable, production-ready code |
Without_tt |
Pre-T4 baseline (manual generic classes) |
T4 |
T4 template development |
Reporting Issues
Use GitHub Issues with:
- Steps to reproduce
- Expected vs. actual behavior
- .NET SDK version and test framework
Repository Statistics
- Created: January 16, 2026 (46 days ago)
- Language: C#
- Size: ~7,223 KB
- Stars: β 1
- Forks: 0
- Open Issues: 0
- License: MIT
- Visibility: Public
View Recent Commits | View All Activity
Why Portamical?
Portamical elevates test data from a framework concern to a domain concern. It treats test cases as immutable, identity-driven value objects, enabling:
- β Portability: Write once, run on xUnit, MSTest, and NUnit
- β Strong Typing: Generics up to 9 arguments (T4-generated)
- β
Deduplication: Automatic via identity-based
HashSet<INamedCase> - β Self-Documentation: Test names read like specifications
- β
Immutability:
init-only properties throughout - β Zero Boilerplate: Factory pattern + T4 code generation
- β
Unified Assertions:
PortamicalAssertwith delegate injection
Ideal For
- Large test suites (500+ parameterized tests)
- Multi-framework environments
- Domain-heavy logic with many edge cases
- Projects needing human-readable test reports
- Teams needing clarity, consistency, and maintainability
License and project lineage
This project is licensed under the MIT License.
Portamical.Core is the continuation and successor of CsabaDu.DynamicTestData.Core (also MIT-licensed).
CsabaDu.DynamicTestData.Core is considered legacy and is no longer supported; new development happens in Portamical.
What changed compared to CsabaDu.DynamicTestData.Core?
Portamical continues the original ideas, with important corrections and refinements:
- Data model: moved away from a record-based model (which proved to be a wrong fit) to immutable classes.
- Identity: improved test case name construction and identity handling:
- more effective name construction (Span-based)
- deduplication via a comparer
- Naming/clarity: several concepts were renamed for readability and long-term maintainability (e.g.,
PropsCodevalues and related terms).
Migration guidance (high level)
If you are using CsabaDu.DynamicTestData.Core:
- Prefer migrating to
Portamical.Corefor continued support and improvements. - Expect mostly mechanical renames, restructured namespaces, plus updates where the API surface changed due to the move from records to immutable classes.
If you want, we can add a dedicated MIGRATION.md with:
- package replacement mapping
- namespace/type rename table
- common before/after snippets
Links
Made by CsabaDu
Portamical: Test data as a domain, not an afterthought.
| Product | Versions 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. |
-
net10.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Portamical.Core:
| Package | Downloads |
|---|---|
|
Portamical
Shared utilities and base classes for crossβframework test data solutions in .NET, built on the Portamical.Core data model. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial releas