Work with a type known only at run time
Domain code and the integrations use the typed path — Iban.TryCreate, or the static abstract members of
IValueObject<TSelf, TValue> through a type parameter — which neither boxes nor allocates. Some code only has a
Type: a generic importer, a tool that reads configuration, an integration of your own. That code goes through
the registry, in AdCodicem.ValueObjects.Metadata.
if (ValueObjectRegistry.TryGet(type, out var descriptor)
&& descriptor.TryParse(text, CultureInfo.InvariantCulture, out var boxed, out var validation))
{
// boxed is the value object, as object
}
A descriptor exposes what the type declares and how to build one:
ValueObjectTypeandValueType, the value object and its underlying type;Schema, the declared rules — lengths, pattern, bounds, format, known values — as data;Create,TryCreate,CreateUncheckedandTryParse, which take and return boxed values;GetValueandFormat, to read an instance back.
A rejection carries the same ValidationResult as the typed path, with the code of the rule that fired. Create
and TryCreate reject a null as value_object.required whatever the underlying type: a decimal value object
does not read it as zero.
Cheaper questions
ValueObjectRegistry.IsValueObject(type) and ValueObjectRegistry.GetUnderlyingType(type) answer without building
a descriptor. Like TryResolve, they unwrap Nullable<T>, which is what a model binder or a serializer is usually
holding.
A value object is a struct implementing IValueObject<TSelf, TValue> over itself. IsValueObject answers true
exactly for the types TryResolve describes, and false for an interface, a class, or a struct carrying only the
IValueObject marker or IValueObject<TValue>. GetUnderlyingType answers null for the same types, even one that
declares a value through IValueObject<TValue>. The integrations claim a type by the same rule: the JSON converter
factory, the Newtonsoft.Json converter, the MVC model binder and MustParseAs leave anything else to the framework,
or refuse it.
Generic value objects
A generic value object registers its generic definition, since its registration knows none of its constructions:
ValueObjectRegistry.GetRegisteredGenericDefinitions() lists them. TryResolve describes a construction the first time
it is asked for it, from the schema and the converter the generator wrote on it, and caches the descriptor;
TryGet finds it from then on. Describing it takes reflection and dynamic code, so under native AOT, register each
construction you look up with ValueObjectRegistry.Register<TSelf, TValue>(TSelf.Schema, new TSelf.ValueJsonConverter()).
When a type is not found
Nothing needs registering by hand: every value object joins the registry through a generated module
initializer. A module initializer only runs once its assembly is loaded, though, so code that looks a type up
before anything else has touched that assembly can call
ValueObjectRegistry.EnsureAssemblyRegistered(assembly) first. The EF Core and Dapper entry points already do.