.NET 10 InlineArray Explicit Size: Fix TypeLoadException with a Wrapper
.NET 10 InlineArray explicit Size is a small compatibility change with an unusually late failure mode. A project can compile, start normally, and then throw TypeLoadException when the runtime first loads a legacy value type that combines InlineArrayAttribute with StructLayoutAttribute.Size. I would not fix this by deleting the size blindly. The explicit byte count probably represented an interop assumption, so the safer job is to identify what the size meant and move that layout intent to an unambiguous wrapper. The complete runnable sample creates the invalid metadata in memory, proves that .NET 10 rejects it, and verifies two supported replacements without a native library or external service. Why the loader rejects the old shape An inline array already defines its storage: the runtime repeats the struct's single field for the length supplied to InlineArrayAttribute. This declaration therefore describes eight consecutive int values: [InlineArray(8)] struct Int8InlineArray { private int _element0; } On a normal .NET 10 target, that type occupies 32 bytes. Adding a second size declaration to the same type creates two competing descriptions of its layout: [InlineArray(8)] [StructLayout(LayoutKind.Sequential, Size = 32)] struct LegacyInt8InlineArray { private int _element0; } Microsoft documents this as a .NET 10 binary compatibility change. Earlier runtimes allowed implementation-specific behavior. .NET 10 rejects the combination when the type is loaded because any interpretation of the duplicate size information would be ambiguous. That timing matters. This is not necessarily a compiler diagnostic in source you control. The shape can already exist in a referenced assembly, and a cold path involving reflection, interop registration, or serialization might be the first code that forces the type to load. Reproduce the TypeLoadException safely I wanted the sample to demonstrate the loader rule without committing an intentionally broken assembly. It uses Reflection.Emit to create the old metadata at runtime: var type = module.DefineType( "LegacyInt8InlineArray", TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.SequentialLayout, typeof(ValueType), PackingSize.Unspecified, typesize: 32); var constructor = typeof(InlineArrayAttribute) .GetConstructor([typeof(int)])!; type.SetCustomAttribute(new CustomAttributeBuilder(constructor, [8])); type.DefineField("_element0", typeof(int), FieldAttributes.Private); _ = type.CreateTypeInfo(); // TypeLoadException on .NET 10 The verifier catches the exception and checks its type. It then uses Unsafe.SizeOf() and fixed values to prove that each replacement is 32 bytes and still supports all eight indexed values. Running the check repeatedly produces identical output, so it works well as an upgrade regression test. There is a useful testing detail here: keep the invalid shape behind an isolated loader boundary. If I referenced a broken type directly throughout the test executable, the runtime could force it while compiling a method, before the assertion reached its intended try block. Emitting one fixture keeps the failure local and makes the expected exception part of the test result instead of a process-level surprise. The InlineArrayAttribute documentation is useful context here: the attribute represents sequentially replicated storage. It should be the only mechanism defining the inline array's total repeated shape. Fix .NET 10 InlineArray explicit Size with a wrapper The correct replacement depends on the original intent. If 32 bytes describes the whole native buffer, wrap the inline array and put Size on the outer type: [InlineArray(8)] struct Int8InlineArray { private int _element0; } [StructLayout(LayoutKind.Sequential, Size = 32)] struct WholeArrayWrapper { public Int8InlineArray Values; } If the explicit size describes one native element, move it to the element and let the inline array repeat that type: [StructLayout(LayoutKind.Sequential, Size = 4)] readonly struct SizedElement(int value) { public int Value { get; } = value; } [InlineArray(8)] struct SizedElementArray { private SizedElement _element0; } This separation makes the contract reviewable: InlineArray(8) owns repetition, while StructLayout.Size owns either the wrapper boundary or an individual element. The StructLayout size reference also warns that an explicit size must be at least as large as the type's fields. What I would gate during an upgrade I would start by scanning source and generated interop code for types that contain both attributes. Then I would load every relevant plugin or interop assembly in a .NET 10 test process, because a successful build does not prove that every value type can be loaded. That load test should use the same deployment shape as production. Trimming, ahead-of-time compilation, plugin discovery, and architecture-specific assemblies can force types in a different order from a normal developer run. I would also make the test enumerate known boundary types explicitly; waiting for incidental application coverage can leave a rarely used native message or device structure unchecked. For each hit, I would record whether the byte count belongs to the element, the repeated buffer, or an enclosing native record. After moving the attribute, I would keep three checks close to the boundary: the managed size is the expected fixed value; representative values survive indexed reads and writes; and the real native declaration agrees on field order, packing, and alignment. The sample and its merged pull request cover the first two checks. They do not prove a platform-specific ABI, and Reflection.Emit is only a controlled way to exercise the loader rule. If the type crosses a P/Invoke boundary, test against the actual native library and every supported architecture. If no native or serialized contract depends on an explicit byte count, a plain inline array may be all you need. How are you auditing layout-sensitive types before your .NET 10 rollout? Happy coding!
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to