Migration from v9 to v10 - PawelGerr/Thinktecture.Runtime.Extensions GitHub Wiki

This guide covers breaking changes and new features when migrating from v9 to v10.

Breaking Changes Summary

Framework Support

  • Dropped support for .NET 7 - Minimum version is now .NET 8.0
  • Dropped support for Entity Framework Core 7 - Minimum version is now EF Core 8.0
  • Minimum .NET SDK increased to 8.0.416

Obsolete Types and Members Removed

All types and members previously marked with [Obsolete] have been deleted. This includes:

Entity Framework Core:

  • UseValueObjectValueConverter → Use UseThinktectureValueConverters instead
  • AddValueObjectConverters → Use AddThinktectureValueConverters instead

Interfaces:

  • IEnum<TKey> → Use ISmartEnum<TKey> instead
  • IEnum<TKey, T, TValidationError> → Use ISmartEnum<TKey, T, TValidationError> instead
  • IValueObjectFactory<TValue> → Use IObjectFactory<TValue> instead
  • IValueObjectFactory<T, TValue, TValidationError> → Use IObjectFactory<T, TValue, TValidationError> instead
  • IComplexValueObject interface deleted (implementation was already generated automatically)

Enums:

  • ValueObjectAccessModifier enum → Use AccessModifier enum instead

ASP.NET Core:

  • TrimmingSmartEnumModelBinder class deleted

Attributes:

  • Several obsolete attribute constructor overloads removed

Swashbuckle Update

  • Updated Swashbuckle.AspNetCore.SwaggerGen to 10.0.1 - If you use the Swashbuckle integration package, ensure compatibility with Swashbuckle 10.x

Other API Changes

  • SmartEnumAttribute<TKey>.KeyMemberType is read-only - The setter has been removed. Assigning the property never had any effect, because the key type always comes from the type argument TKey. Removing the assignment from your code does not change any behavior, but code that still assigns the property no longer compiles.
  • TxIsNullableReferenceType getters report every non-value type - The getters T1IsNullableReferenceType to T5IsNullableReferenceType of AdHocUnionAttribute and of the generic UnionAttribute<T1, T2, ...> family now return true for a stateless member of any non-value type, for example an interface. Previously they returned true only for classes. This affects the reflection surface of the attribute instance; the generated code is unchanged, because the source generator determines reference types from the compilation model.

New and Widened Analyzer Rules

v10 adds the analyzer rules TTRESG079 (Error), TTRESG108 (Warning) and TTRESG109 (Warning), and it widens TTRESG078 from ReadOnlySpan<char> factories to object factories with any ref-struct value type. Each rule flags a configuration that already failed at build time or at runtime, or was silently ignored before, so your code behaves as it did. Still, an existing project can report new build diagnostics right after the upgrade. See Analyzer Diagnostics for the full list.

Entity Framework Core Configuration (BREAKING CHANGE)

v10 introduces a new, more structured configuration API for Entity Framework Core integration. The old callback-based approach is deprecated but still supported for backward compatibility.

What Changed

The UseThinktectureValueConverters method now accepts a Configuration object that provides:

  • Type-safe configuration for Smart Enums and Keyed Value Objects
  • Built-in max length strategies for automatic database column sizing
  • Better defaults with smart enum max length calculation out-of-the-box

Old API (v9 - Deprecated)

.UseThinktectureValueConverters(configureEnumsAndKeyedValueObjects: property =>
{
   if (property.ClrType == typeof(ProductType))
   {
      var maxLength = ProductType.Items.Max(i => i.Key.Length);
      property.SetMaxLength(maxLength + (10 - maxLength % 10)); // round up to next 10
   }
   else if (property.ClrType == typeof(ProductName))
   {
      property.SetMaxLength(200);
   }
})

New API (v10 - Recommended)

Option 1: Use default configuration (automatic max length for string-based smart enums)

.UseThinktectureValueConverters()
// or explicitly:
.UseThinktectureValueConverters(Configuration.Default)

Option 2: Custom configuration with strategies

.UseThinktectureValueConverters(new Configuration
{
   SmartEnums = new SmartEnumConfiguration
   {
      // Automatically calculates max length from items and rounds to next 10
      MaxLengthStrategy = DefaultSmartEnumMaxLengthStrategy.Instance
   },
   KeyedValueObjects = new KeyedValueObjectConfiguration
   {
      MaxLengthStrategy = new CustomKeyedValueObjectMaxLengthStrategy((type, keyType) =>
      {
         if (type == typeof(ProductName))
            return 200;

         return MaxLengthChange.None; // Max length stays unchanged
      })
   }
})

Behavioral Changes

The following changes do not break compilation, but they change what your code does at runtime.

Factory Methods and Parsing

  • TryCreate no longer promises a non-null result when NullInFactoryMethodsYieldsNull = true - With this setting, TryCreate(null, out var obj) returns true and leaves obj as null. The generated out parameter therefore no longer carries [NotNullWhen(true)]. The runtime behavior is unchanged; only the nullability contract now describes it correctly. As a result, the compiler can report new CS8602 warnings where you dereference the result after a successful call. Add a null check to fix them. See Null Value Handling.
  • Parse and TryParse reject empty input when EmptyStringInFactoryMethodsYieldsNull = true - For a string-keyed reference Value Object with this setting, the generated Parse now throws a FormatException for empty or whitespace-only input. It previously returned null even though the return type is non-nullable. TryParse now returns false for such input instead of returning true with a null result. Because ASP.NET Core binds parameters through IParsable<T> and ISpanParsable<T> (minimal APIs, for example), empty input that used to bind to null now fails to bind. The MVC model binder of the ASP.NET Core integration package, the JSON and MessagePack integrations, and the factory methods Create, TryCreate and Validate are unchanged: empty input still yields null there. See Empty String Handling.

Collections

  • Set comparison methods reject null - The sets returned by Empty.Set<T>() and SingleItem.Set<T>(item) now throw an ArgumentNullException with the parameter name other when you pass null to IsSubsetOf, IsSupersetOf, IsProperSubsetOf, IsProperSupersetOf, Overlaps or SetEquals. Previously some of these methods returned true or false for a null argument, and others threw with the parameter name source. The new behavior matches HashSet<T>.
  • Dictionary members reject null keys - The dictionaries returned by Empty.Dictionary<TKey, TValue>() and SingleItem.Dictionary(key, value) now throw an ArgumentNullException with the parameter name key when you pass a null key to the indexer, ContainsKey or TryGetValue. SingleItem.Dictionary also rejects a null key at creation time. Previously ContainsKey and TryGetValue returned false and the indexer threw a KeyNotFoundException. The new behavior matches Dictionary<TKey, TValue>.

Serialization

  • Span-based JSON deserialization rejects non-string tokens (.NET 9+) - On .NET 9 and later, the span-based System.Text.Json converter now throws a JsonException for any token that is neither a string nor a property name. Numbers and booleans were silently transcoded from their raw bytes and accepted before. This converter is used by string-based Smart Enums by default and by types with an [ObjectFactory<ReadOnlySpan<char>>(UseForSerialization = SerializationFrameworks.SystemTextJson)]. On .NET 8 such input already threw.
  • MessagePack serializes items of derived Smart Enum types - Serializing an item through the runtime type of a derived (nested) Smart Enum class no longer throws an InvalidCastException. The resolver now wraps the formatter of the base type in a casting formatter for derived item types. A [MessagePackFormatter] attribute applied directly to the derived class is still honored.
  • Object factories with a ref-struct value type are ignored instead of failing - MessagePack and Newtonsoft.Json now ignore an object factory whose value type is a ref struct, and System.Text.Json as well as the Swashbuckle schema filter ignore every ref-struct factory except ReadOnlySpan<char>. These frameworks fall back to the key-based conversion instead of failing during type initialization. The analyzer reports TTRESG108 for such a configuration.

Entity Framework Core

  • Primitive collections of string-based Smart Enums receive the max length on the element - The max length strategy now configures the element type of a primitive collection instead of the collection property itself. Previously the collection column got the length of a single item, for example nvarchar(10) for the whole collection. When you upgrade from v9 or from an earlier 10.x release, EF Core scaffolds a new migration for the affected columns.
  • Max length strategies skip types with an Entity Framework object factory - The built-in max length strategies no longer apply to a type that has an [ObjectFactory<T>(UseWithEntityFramework = true)], because the factory decides the persisted value and that value may be longer than the key. Columns that were limited automatically before are now unlimited, so expect a new EF migration. Configure the length explicitly with HasMaxLength if you need the limit. See Configuration and Max Length Strategies.

OpenAPI / Swashbuckle

  • The AllOf Smart Enum schema no longer emits a per-item title - The AllOf variant now combines all items into a single enum subschema, because a conjunction of per-item const subschemas can never be satisfied. Set SmartEnumSchemaExtension = SmartEnumSchemaExtension.VarNamesFromStringRepresentation (or VarNamesFromDotnetIdentifiers) to expose the item names through the x-enum-varnames extension instead. See Available Options.

Discriminated Unions: Configurable Parameter Names in Switch/Map

v10 adds the NestedUnionParameterNames option for configuring the Switch and Map parameter names of nested Regular Unions. This is not a breaking change: the default behavior is unchanged. By default the parameter names still include the intermediate type names to avoid conflicts.

Default (unchanged):

failure.Switch(
   failureNotFound: notFound => ...,
   failureUnauthorized: unauthorized => ...
);

Set NestedUnionParameterNames = NestedUnionParameterNameGeneration.Simple on the union to opt into the shorter names that omit the intermediate type names:

[Union(NestedUnionParameterNames = NestedUnionParameterNameGeneration.Simple)]

failure.Switch(
   notFound: notFound => ...,
   unauthorized: unauthorized => ...
);

See Configuring nested union parameter names for details.

⚠️ **GitHub.com Fallback** ⚠️