C# SDK
Immutable C# runtime for the bounded Morphir .NET SDK contract
Morphir.SDK.CSharp is an independent net10.0 runtime package. It uses the BCL
and System.Collections.Immutable, with no FSharp.Core, Morphir compiler, model,
CLI or Roslyn dependency. Existing Morphir.SDK consumers remain unchanged.
Install the package version published for your application, then use the
Morphir.SDK.CSharp namespace. Both C#14 and C#15 consumers use the same API.
The SDK does not require C#15 union syntax or a source generator.
using Morphir.SDK.CSharp;
var optional = Maybe<int>.Just(7).Map(x => Basics.Add(x, 1));
var result = Result<string, int>.Ok(7)
.AndThen(x => x > 0
? Result<string, int>.Ok(x)
: Result<string, int>.Err("negative"));
var old = Dict<string, int>.Singleton("a", 1, MorphirComparers.String);
var updated = old.Insert("b", 2);
// old still contains only a; updated enumerates a then b.
Use the API guide for constructors, immutable collections and error
behavior. The compatibility guide describes the bounded
contract, compiler profiles and runtime publication checks.
1 - C# SDK API
All public runtime types live in Morphir.SDK.CSharp. The frozen inventory uses
logical Morphir names such as List.map; C# uses PascalCase instance methods
and generic factories. Its parameter/result meanings are unchanged.
| Morphir vocabulary | Public C# API |
|---|
| Maybe.Just, Nothing | Maybe<T>.Just(value), Maybe<T>.Nothing |
| Maybe.map, andThen | value.Map(callback), value.AndThen(callback) |
| Result.Ok, Err | Result<TError,TValue>.Ok(value), .Err(error) |
| Result.map, andThen | result.Map(callback), result.AndThen(callback) |
| Unit; Basics.LT, EQ, GT | Unit.Value; Order.LT, .EQ, .GT, also Basics constants |
| List literal, singleton, cons | List<T>.FromEnumerable(values), .Singleton(value), list.Cons(head) |
| List.map, append, head, tail, length | list.Map(f), .Append(right), .Head(), .Tail(), .Length |
| Dict.empty, singleton, fromList | Dict<K,V>.Empty(comparer), .Singleton(key,value,comparer), .FromEnumerable(pairs,comparer) |
| Dict.get, insert, update, remove, toList, union | dict.Get(key), .Insert(key,value), .Update(key,f), .Remove(key), .ToList(), .Union(right) |
| Set.empty, singleton, fromList | Set<T>.Empty(comparer), .Singleton(value,comparer), .FromEnumerable(values,comparer) |
| Set.insert, remove, toList, union | set.Insert(value), .Remove(value), .ToList(), .Union(right) |
| Basics scalar operations | Basics.Add, Subtract, Multiply, Divide, IntegerDivide, ModBy, RemainderBy, Negate, Abs, Power, ToFloat, Round, Floor, Ceiling, Truncate, IsNaN, IsInfinite, Compare, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual, Min, Max, Clamp, Equal, NotEqual |
| Decimal operations | Decimal.Add, Sub, Mul, Div, Compare, ToString |
Use an alias such as using SdkDecimal = Morphir.SDK.CSharp.Decimal; when
System.Decimal is also imported. SDK List<T> may similarly need an alias
when System.Collections.Generic is imported outside the SDK namespace.
Maybe<T> is a readonly struct whose default is Nothing. Result<E,V> is a
sealed immutable class with private construction, error type first and success
type second. Neither uses null to encode a branch. Match observes either
variant without throwing for absence. Constructors reject null payloads.
Nothing and Err bypass map/bind callbacks. Called callback exceptions propagate
without translation to Nothing or Err.
var text = Result<string, int>.Err("negative")
.Match(error => error, value => value.ToString());
var absent = Maybe<int>.Nothing.Match(value => value, () => 0);
Collection factories copy enumerable inputs into immutable storage. Lists use
ImmutableList, dictionaries ImmutableSortedDictionary, and sets
ImmutableSortedSet. Public observations expose read-only enumeration and no
builder, mutable array or writable backing collection. All updates return new
immutable values. Dictionary construction retains the last duplicate value;
union retains the left dictionary’s overlapping value. Update receives
Just current or Nothing and either inserts Just value or removes Nothing.
using SdkList = Morphir.SDK.CSharp.List<int>;
var original = SdkList.FromEnumerable(new[] { 1, 2 });
var mapped = original.Map(x => Basics.Add(x, 1));
var keys = MorphirComparers.List(MorphirComparers.Int);
var dict = Dict<SdkList, Maybe<SdkList>>.Singleton(
original, Maybe<SdkList>.Just(mapped), keys);
var equal = Dict<SdkList, Maybe<SdkList>>.Singleton(
SdkList.FromEnumerable(new[] { 1, 2 }),
Maybe<SdkList>.Just(SdkList.FromEnumerable(new[] { 2, 3 })), keys);
// dict.Equals(equal) and equal in-process hash codes.
Choose explicit MorphirComparers.Int, .Float, .Char, .String,
.List(elementComparer) or .Tuple(...) for 2/3-tuples. Value validates boxed
comparable values recursively and supports adapter-style heterogeneous object
storage; individual list/key types must still respect the frozen homogeneous
domain. There is no culture-sensitive CLR fallback. Bool, Decimal, Unit, Order,
Maybe, Result, Dict, Set and callbacks have no admitted ordering. Float ordered
keys must be finite. String ordering is ordinal UTF-16, without normalization.
SDK values implement structural Equals and consistent GetHashCode, including
nested variants, collections and tuples. Decimal scale and signed floating zero
do not distinguish equal hashes. NaN compares unequal, including inside SDK
variants. Hash numbers are not a serialized format or cross-process contract.
Generated records and variants opt in through the small IMorphirValue protocol.
Implement MorphirEquals by checking the exact case/type and comparing every
field with Basics.Equal. Implement MorphirHashCode with MorphirValue.Hash
over the same fields. For C# record/native equality, route the typed Equals
and GetHashCode overrides to those methods as well. This avoids CLR NaN
equality and works after boxing or inside tuples, variants and collections.
sealed record Amount(double Number, Morphir.SDK.CSharp.List<int> Values)
: IMorphirValue
{
public bool MorphirEquals(IMorphirValue other) => other is Amount x
&& Basics.Equal(Number, x.Number) && Basics.Equal(Values, x.Values);
public int MorphirHashCode() => MorphirValue.Hash((Number, Values));
public bool Equals(Amount? other) => other is not null && MorphirEquals(other);
public override int GetHashCode() => MorphirHashCode();
}
The protocol is an explicit caller contract. An interface cannot enforce
physical immutability or correct custom equality. Generated code and application
authors must keep payloads immutable and implement all fields consistently.
Ordinary IEquatable objects, arrays, object identity and delegates do not
automatically become Morphir values. MorphirValue.Hash is a .NET composition
helper, not an extension to the frozen logical operation inventory.
Integers are checked Int32. Overflow raises OverflowException; integer zero
division raises DivideByZeroException. RemainderBy(divisor, dividend) uses
the dividend sign; ModBy uses the divisor sign. minInt % -1 is zero.
Float is IEEE binary64. Half rounding resolves toward positive infinity.
Float-to-Int conversion raises NonFiniteConversionException for NaN/infinity
and OverflowException for an out-of-range rounded value. Clamp tests low first,
then high, and preserves the selected value, including floating zero signs.
Decimal operates on the exact Decimal96 subset. Results remove redundant scale;
Decimal.ToString uses invariant positional notation without trailing zeros.
Decimal.Div(a,0) returns Nothing. Exact overflow raises OverflowException.
Nonterminating fractions, results requiring scale above 28, negative Int powers,
and unsupported ordering/equality domains raise NotSupportedException. They do
not silently round, wrap, clamp or acquire CLR reference semantics.
2 - C# SDK compatibility
The bounded contract is morphir-dotnet-sdk-v1, schema 1, profile
dotnet-sdk-v1: 67 operations, 207 required observations and eight separately
reported representation exclusions. It does not claim the entire Morphir SDK.
The checked-in fixtures retain independent upstream source evidence. Runtime
test expectations are read from those fixtures, not computed by this runtime.
The excluded observations concern Int range or negative Int powers,
supplementary Char, and exact Decimal96 representation/nontermination limits.
The precise IDs and reasons remain in tests/Fixtures/MorphirSDK/cases.json.
Strings support well-formed UTF-16 including supplementary scalars. Char accepts
one non-surrogate BMP scalar. Dates, parsing, transcendental operations and
unlisted collection functions are outside this inventory.
The runtime targets net10.0 and works with trimming and NativeAOT without
reflection or dynamic code discovery. Runtime targeting is separate from the
compiler SDK and numeric language version used by a consumer. Root global.json
is not changed for isolated compiler verification. Ordinary package consumption
must restore only Morphir.SDK.CSharp, without a project reference or repository
source files. This is separate from the deferred Roslyn generator adapter.
The same ordinary consumer source was restored from an isolated local feed,
compiled and executed under both exact toolchains below. Each fresh package
cache contained only Morphir.SDK.CSharp; neither consumer referenced repository
projects or source files.
| Numeric language version | SDK/channel | Exact C# compiler build |
|---|
| 14.0 | 10.0.403, stable | 5.9.0-1.26523.112, revision 5a2162bea6845eabc1fc721624a43568c65db3b1 |
| 15.0 | 11.0.100-rc.2.26504.105, RC2 | 5.12.0-1.26504.105, revision 820230c386db5b0106c5430b3e5aee92a7826bf5 |
Both target net10.0 and execute with runtime 10.0.14. SDK selection is disabled
from rolling forward in each isolated global.json. The RC toolchain uses its
matching 10.0.13 reference packs for net10.0 compilation; these tooling inputs
are separate from package dependencies. The RC ZIP was checked against
Microsoft’s release metadata.
No root global.json, user setting or machine setting changed.
The same public consumer was also published and executed as self-contained
trimmed and NativeAOT applications on Windows arm64, RID win-arm64, using SDK
10.0.403 and runtime 10.0.14. Both passed the ordinary consumer’s assertions and
produced the same output. Trim and AOT warnings were treated as errors. The
NativeAOT executable is an ARM64 native PE without a CLR header or managed
Consumer.dll/coreclr.dll payload. This verifies that representative runtime
consumer on that OS/RID, not every deployment target or every generic closure.
Reproduce with the checked-in PowerShell harness and installed exact toolchains:
& tests/Morphir.SDK.CSharp.Tests/verify-package-consumers.ps1 `
-EvidenceDirectory C:/Temp/owned-fresh-sdk-proof `
-StableDotnet C:/path/to/stable/dotnet.exe `
-RcDotnet C:/path/to/isolated-rc/dotnet.exe
The RC directory needs its matching net10.0 targeting packs. NativeAOT needs
Microsoft’s native prerequisites.
The harness uses fresh local feeds/caches and process-local environment values,
runs commands serially with bounded parallelism, and preserves logs on failure.
SDK/compiler versions and numeric language versions are separate settings. This
proof does not require native C# unions or implement the deferred source generator.