Skip to content

Commit

Permalink
Merge pull request #167 from AArnott/36-support-for-open-set-subtype-…
Browse files Browse the repository at this point in the history
…unions

Improve known sub-type support
  • Loading branch information
AArnott authored Dec 14, 2024
2 parents fa6bd7c + 66bc4a0 commit 627825e
Show file tree
Hide file tree
Showing 23 changed files with 1,404 additions and 110 deletions.
7 changes: 4 additions & 3 deletions docfx/docs/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Feature | Nerdbank.MessagePack | MessagePack-CSharp |
Optimized for high performance | [](performance.md) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#performance) |
Contractless data types | [](getting-started.md)[^1] | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#object-serialization) |
Attributed data types | [](customizing-serialization.md) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#object-serialization) |
Polymorphic serialization | [](unions.md) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#union) |
Polymorphic serialization | [](unions.md) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#union)[^4] |
Skip serializing default values | [](xref:Nerdbank.MessagePack.MessagePackSerializer.SerializeDefaultValues) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp/issues/678) |
Dynamically use maps or arrays for most compact format | [](customizing-serialization.md#array-or-map) | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp/issues/1953) |
Typeless serialization | ❌ | [](https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#typeless) |
Expand All @@ -41,6 +41,7 @@ Security is a complex subject, and an area where Nerdbank.MessagePack is activel
[^1]: Nerdbank.MessagePack's approach is more likely to be correct by default and more flexible to fixing when it is not.
[^2]: Although MessagePack-CSharp does not support .NET 8 flavor NativeAOT, it has long-supported Unity's il2cpp runtime, but it requires careful avoidance of dynamic features.
[^3]: This hasn't been tested, and even if it works, the level of active support may be limited as the maintainers of Nerdbank.MessagePack do not use Unity. We may accept outside contributions to support it if it isn't onerous to maintain.
[^4]: MessagePack-CSharp is limited to derived types that can be attributed on the base type, whereas Nerdbank.MessagePack allows for dynamically identifying derived types at runtime.

## Migration process

Expand Down Expand Up @@ -114,8 +115,8 @@ Nerdbank.MessagePack supports this same use case via its @Nerdbank.MessagePack.K
```diff
-[Union(0, typeof(MyType1))]
-[Union(1, typeof(MyType2))]
+[KnownSubType(0, typeof(MyType1))]
+[KnownSubType(1, typeof(MyType2))]
+[KnownSubType(typeof(MyType1), 0)]
+[KnownSubType(typeof(MyType2), 1)]
public interface IMyType
{
}
Expand Down
79 changes: 79 additions & 0 deletions docfx/docs/unions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This changes the schema of the serialized data to include a tag that indicates t
```

But with the `KnownSubTypeAttribute`, it serializes like this:

```json
[null, { "Name": "Bessie" }]
```
Expand Down Expand Up @@ -69,6 +70,7 @@ Now suppose you have different breeds of horses that each had their own subtype:
---

At this point your `HorsePen` *would* serialize with the union schema around each horse:

```json
{ "Horses": [[1, { "Name": "Bessie" }], [2, { "Name", "Lightfoot" }]] }
```
Expand All @@ -79,6 +81,61 @@ The `Animal` class only knows about `Horse` as a subtype and designates `2` as t
As such, serializing your `Farm` would drop any details about horse breeds and deserializing would produce `Horse` objects, not `QuarterHorse` or `Thoroughbred`.
To fix this, you would need to add @Nerdbank.MessagePack.KnownSubTypeAttribute`1 to the `Animal` class for `QuarterHorse` and `Thoroughbred` that assigns type aliases for each of them.

### Alias types

An alias may be an integer or a string.
String aliases are case sensitive.

Aliases may also be inferred from the @System.Type.FullName?displayProperty=nameWithType of the sub-type, in which case they are treated as strings.

The following example shows using strings:

# [.NET](#tab/net)

[!code-csharp[](../../samples/Unions.cs#StringAliasTypesNET)]

# [.NET Standard](#tab/netfx)

[!code-csharp[](../../samples/Unions.cs#StringAliasTypesNETFX)]

---

Mixing alias types for a given base type is allowed, as shown here:

# [.NET](#tab/net)

[!code-csharp[](../../samples/Unions.cs#MixedAliasTypesNET)]

# [.NET Standard](#tab/netfx)

[!code-csharp[](../../samples/Unions.cs#MixedAliasTypesNETFX)]

---

Following is an example of string alias inferrence:

# [.NET](#tab/net)

[!code-csharp[](../../samples/Unions.cs#InferredAliasTypesNET)]

# [.NET Standard](#tab/netfx)

[!code-csharp[](../../samples/Unions.cs#InferredAliasTypesNETFX)]

---

Note that while inferrence is the simplest syntax, it results in the serialized schema including the full name of the type, which can make the serialized form more fragile in the face of refactoring changes.
It can also result in a poorer experience if the data is exchanged with non-.NET programs.

### Nested sub-types

Suppose you had the following type hierarchy:

Animal <- Horse <- Quarterback

The `Animal` class _must_ have the whole set of transitive derived types listed as known sub-types directly on itself.
It will not do for `Animal` to merely mention `Horse` and for `Horse` to listed `Quarterback` as a sub-type, as this is not currently supported.

### Generic sub-types

Sub-types may be generic types, but they must be *closed* generic types (i.e. all the generic type arguments must be specified).
Expand All @@ -98,3 +155,25 @@ For example:
[!code-csharp[](../../samples/Unions.cs#ClosedGenericSubTypesNETFX)]

---

### Runtime subtype registration

Static registration via attributes is not always possible.
For instance, you may want to serialize types from a third-party library that you cannot modify.
Or you may have an extensible plugin system where new types are added at runtime.
Or most simply, the derived types may not be declared in the same assembly as the base type.

In such cases, runtime registration of subtypes is possible to allow you to run any custom logic you may require to discover and register subtypes.
Your code is still responsible to ensure unique aliases are assigned to each subtype.

Consider the following example where a type hierarchy is registered without using the attribute approach:

# [.NET](#tab/net)

[!code-csharp[](../../samples/Unions.cs#RuntimeSubTypesNET)]

# [.NET Standard](#tab/netfx)

[!code-csharp[](../../samples/Unions.cs#RuntimeSubTypesNETFX)]

---
186 changes: 178 additions & 8 deletions samples/Unions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ public partial class Dog : Animal { }
#endregion
#else
#region FarmAnimalsNETFX
[KnownSubType(1, typeof(Cow))]
[KnownSubType(2, typeof(Horse))]
[KnownSubType(3, typeof(Dog))]
[KnownSubType(typeof(Cow), 1)]
[KnownSubType(typeof(Horse), 2)]
[KnownSubType(typeof(Dog), 3)]
public class Animal
{
public string? Name { get; set; }
Expand Down Expand Up @@ -66,8 +66,8 @@ public partial class Thoroughbred : Horse { }
#endregion
#else
#region HorseBreedsNETFX
[KnownSubType(1, typeof(QuarterHorse))]
[KnownSubType(2, typeof(Thoroughbred))]
[KnownSubType(typeof(QuarterHorse), 1)]
[KnownSubType(typeof(Thoroughbred), 2)]
public partial class Horse : Animal { }

[GenerateShape]
Expand Down Expand Up @@ -105,9 +105,9 @@ class ClovenHoof { }
#endregion
#else
#region ClosedGenericSubTypesNETFX
[KnownSubType(1, typeof(Horse))]
[KnownSubType(2, typeof(Cow<SolidHoof>))]
[KnownSubType(3, typeof(Cow<ClovenHoof>))]
[KnownSubType(typeof(Horse), 1)]
[KnownSubType(typeof(Cow<SolidHoof>), 2)]
[KnownSubType(typeof(Cow<ClovenHoof>), 3)]
class Animal
{
public string? Name { get; set; }
Expand All @@ -128,3 +128,173 @@ class ClovenHoof { }
#endregion
#endif
}

namespace StringAliasTypes
{
#if NET
#region StringAliasTypesNET
[GenerateShape]
[KnownSubType<Horse>("Horse")]
[KnownSubType<Cow>("Cow")]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#else
#region StringAliasTypesNETFX
[GenerateShape]
[KnownSubType(typeof(Horse), "Horse")]
[KnownSubType(typeof(Cow), "Cow")]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#endif
}

namespace MixedAliasTypes
{
#if NET
#region MixedAliasTypesNET
[GenerateShape]
[KnownSubType<Horse>(1)]
[KnownSubType<Cow>("Cow")]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#else
#region MixedAliasTypesNETFX
[GenerateShape]
[KnownSubType(typeof(Horse), 1)]
[KnownSubType(typeof(Cow), "Cow")]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#endif
}

namespace InferredAliasTypes
{
#if NET
#region InferredAliasTypesNET
[GenerateShape]
[KnownSubType<Horse>]
[KnownSubType<Cow>]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#else
#region InferredAliasTypesNETFX
[GenerateShape]
[KnownSubType(typeof(Horse))]
[KnownSubType(typeof(Cow))]
partial class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }
#endregion
#endif
}

namespace RuntimeSubTypes
{
#if NET
#region RuntimeSubTypesNET
class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }

class SerializationConfigurator
{
internal void ConfigureAnimalsMapping(MessagePackSerializer serializer)
{
KnownSubTypeMapping<Animal> mapping = new();
mapping.Add<Horse>(1);
mapping.Add<Cow>(2);

serializer.RegisterKnownSubTypes(mapping);
}
}
#endregion
#else
#region RuntimeSubTypesNETFX
class Animal
{
public string? Name { get; set; }
}

[GenerateShape]
partial class Horse : Animal { }

[GenerateShape]
partial class Cow : Animal { }

[GenerateShape<Horse>]
[GenerateShape<Cow>]
partial class Witness;

class SerializationConfigurator
{
internal void ConfigureAnimalsMapping(MessagePackSerializer serializer)
{
KnownSubTypeMapping<Animal> mapping = new();
mapping.Add<Horse>(1, Witness.ShapeProvider);
mapping.Add<Cow>(2, Witness.ShapeProvider);

serializer.RegisterKnownSubTypes(mapping);
}
}
#endregion
#endif
}
20 changes: 20 additions & 0 deletions src/Nerdbank.MessagePack.Analyzers/AnalyzerUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Nerdbank.MessagePack.Analyzers;
Expand Down Expand Up @@ -136,5 +137,24 @@ public static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol symbol)
? a[typeArgumentIndex].GetLocation()
: null;

public static string GetFullName(this INamedTypeSymbol symbol)
{
var sb = new StringBuilder();

if (symbol.ContainingType is not null)
{
sb.Append(GetFullName(symbol.ContainingType));
sb.Append('.');
}
else if (!symbol.ContainingNamespace.IsGlobalNamespace)
{
sb.Append(symbol.ContainingNamespace.MetadataName);
sb.Append('.');
}

sb.Append(symbol.MetadataName);
return sb.ToString();
}

internal static string GetHelpLink(string diagnosticId) => $"https://aarnott.github.io/Nerdbank.MessagePack/analyzers/{diagnosticId}.html";
}
Loading

0 comments on commit 627825e

Please sign in to comment.