Skip to content

Commit e2c9188

Browse files
authored
Improve serialization compatibility with new component types (#1620)
* Improve serialization compatibility with new component types
1 parent dbff730 commit e2c9188

File tree

9 files changed

+444
-55
lines changed

9 files changed

+444
-55
lines changed

src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,12 @@
22
namespace Microsoft.ComponentDetection.Contracts.BcdeModels;
33

44
using System;
5-
using System.Collections.Generic;
65
using Microsoft.ComponentDetection.Contracts.TypedComponent;
76
using Newtonsoft.Json;
87
using Newtonsoft.Json.Linq;
98

109
public class TypedComponentConverter : JsonConverter
1110
{
12-
private readonly Dictionary<ComponentType, Type> componentTypesToTypes = new Dictionary<ComponentType, Type>
13-
{
14-
{ ComponentType.Other, typeof(OtherComponent) },
15-
{ ComponentType.NuGet, typeof(NuGetComponent) },
16-
{ ComponentType.Npm, typeof(NpmComponent) },
17-
{ ComponentType.Maven, typeof(MavenComponent) },
18-
{ ComponentType.Git, typeof(GitComponent) },
19-
{ ComponentType.RubyGems, typeof(RubyGemsComponent) },
20-
{ ComponentType.Cargo, typeof(CargoComponent) },
21-
{ ComponentType.Conan, typeof(ConanComponent) },
22-
{ ComponentType.Pip, typeof(PipComponent) },
23-
{ ComponentType.Go, typeof(GoComponent) },
24-
{ ComponentType.DockerImage, typeof(DockerImageComponent) },
25-
{ ComponentType.Pod, typeof(PodComponent) },
26-
{ ComponentType.Linux, typeof(LinuxComponent) },
27-
{ ComponentType.Conda, typeof(CondaComponent) },
28-
{ ComponentType.DockerReference, typeof(DockerReferenceComponent) },
29-
{ ComponentType.Vcpkg, typeof(VcpkgComponent) },
30-
{ ComponentType.Spdx, typeof(SpdxComponent) },
31-
{ ComponentType.DotNet, typeof(DotNetComponent) },
32-
};
33-
3411
public override bool CanWrite
3512
{
3613
get { return false; }
@@ -46,8 +23,14 @@ public override object ReadJson(
4623
{
4724
var jo = JToken.Load(reader);
4825

49-
var value = (ComponentType)Enum.Parse(typeof(ComponentType), (string)jo["type"]);
50-
var targetType = this.componentTypesToTypes[value];
26+
var typeString = (string)jo["type"];
27+
if (!TypedComponentMapping.TryGetType(typeString, out var targetType))
28+
{
29+
// Unknown component type - return null to allow forward compatibility
30+
// when new component types are added but downstream clients haven't updated yet
31+
return null;
32+
}
33+
5134
var instanceOfTypedComponent = Activator.CreateInstance(targetType, true);
5235
serializer.Populate(jo.CreateReader(), instanceOfTypedComponent);
5336

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#nullable disable
2+
namespace Microsoft.ComponentDetection.Contracts.BcdeModels;
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
7+
8+
/// <summary>
9+
/// Provides a shared mapping between component type discriminator strings and their corresponding concrete TypedComponent types.
10+
/// This mapping is used by both Newtonsoft.Json and System.Text.Json converters for polymorphic serialization.
11+
/// </summary>
12+
internal static class TypedComponentMapping
13+
{
14+
/// <summary>
15+
/// Gets the dictionary mapping type discriminator strings to their corresponding concrete types.
16+
/// The keys are case-insensitive to handle variations in JSON serialization.
17+
/// </summary>
18+
public static IReadOnlyDictionary<string, Type> TypeDiscriminatorToType { get; } = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
19+
{
20+
{ nameof(ComponentType.Other), typeof(OtherComponent) },
21+
{ nameof(ComponentType.NuGet), typeof(NuGetComponent) },
22+
{ nameof(ComponentType.Npm), typeof(NpmComponent) },
23+
{ nameof(ComponentType.Maven), typeof(MavenComponent) },
24+
{ nameof(ComponentType.Git), typeof(GitComponent) },
25+
{ nameof(ComponentType.RubyGems), typeof(RubyGemsComponent) },
26+
{ nameof(ComponentType.Cargo), typeof(CargoComponent) },
27+
{ nameof(ComponentType.Conan), typeof(ConanComponent) },
28+
{ nameof(ComponentType.Pip), typeof(PipComponent) },
29+
{ nameof(ComponentType.Go), typeof(GoComponent) },
30+
{ nameof(ComponentType.DockerImage), typeof(DockerImageComponent) },
31+
{ nameof(ComponentType.Pod), typeof(PodComponent) },
32+
{ nameof(ComponentType.Linux), typeof(LinuxComponent) },
33+
{ nameof(ComponentType.Conda), typeof(CondaComponent) },
34+
{ nameof(ComponentType.DockerReference), typeof(DockerReferenceComponent) },
35+
{ nameof(ComponentType.Vcpkg), typeof(VcpkgComponent) },
36+
{ nameof(ComponentType.Spdx), typeof(SpdxComponent) },
37+
{ nameof(ComponentType.DotNet), typeof(DotNetComponent) },
38+
{ nameof(ComponentType.Swift), typeof(SwiftComponent) },
39+
};
40+
41+
/// <summary>
42+
/// Tries to get the concrete type for a given type discriminator string.
43+
/// </summary>
44+
/// <param name="typeDiscriminator">The type discriminator string from JSON.</param>
45+
/// <param name="targetType">When successful, contains the concrete type; otherwise null.</param>
46+
/// <returns>True if the type discriminator was recognized; otherwise false.</returns>
47+
public static bool TryGetType(string typeDiscriminator, out Type targetType)
48+
{
49+
if (string.IsNullOrWhiteSpace(typeDiscriminator))
50+
{
51+
targetType = null;
52+
return false;
53+
}
54+
55+
return TypeDiscriminatorToType.TryGetValue(typeDiscriminator, out targetType);
56+
}
57+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#nullable disable
2+
namespace Microsoft.ComponentDetection.Contracts.BcdeModels;
3+
4+
using System;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
8+
using TypedComponentClass = Microsoft.ComponentDetection.Contracts.TypedComponent.TypedComponent;
9+
10+
/// <summary>
11+
/// A System.Text.Json converter for TypedComponent that handles unknown component types gracefully.
12+
/// When an unknown component type is encountered, it returns null instead of throwing an exception.
13+
/// This enables forward compatibility when new component types are added but downstream clients haven't updated yet.
14+
/// </summary>
15+
public class TypedComponentSystemTextJsonConverter : JsonConverter<TypedComponentClass>
16+
{
17+
private const string TypePropertyNameCamelCase = "type";
18+
private const string TypePropertyNamePascalCase = "Type";
19+
20+
public override TypedComponentClass Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
21+
{
22+
if (reader.TokenType == JsonTokenType.Null)
23+
{
24+
return null;
25+
}
26+
27+
// Parse the JSON into a document so we can inspect it
28+
using var doc = JsonDocument.ParseValue(ref reader);
29+
var root = doc.RootElement;
30+
31+
// Extract the type discriminator
32+
// Check both camelCase and PascalCase property names
33+
if (!root.TryGetProperty(TypePropertyNameCamelCase, out var typeProperty) &&
34+
!root.TryGetProperty(TypePropertyNamePascalCase, out typeProperty))
35+
{
36+
return null;
37+
}
38+
39+
var typeDiscriminator = typeProperty.GetString();
40+
if (!TypedComponentMapping.TryGetType(typeDiscriminator, out var targetType))
41+
{
42+
// Unknown component type - return null for forward compatibility
43+
return null;
44+
}
45+
46+
// Deserialize to the specific concrete type using default serialization
47+
return (TypedComponentClass)root.Deserialize(targetType, options);
48+
}
49+
50+
public override void Write(Utf8JsonWriter writer, TypedComponentClass value, JsonSerializerOptions options)
51+
{
52+
// Serialize to a document first to get all properties
53+
using var doc = JsonSerializer.SerializeToDocument(value, value.GetType(), options);
54+
55+
writer.WriteStartObject();
56+
57+
// Write the type discriminator first (using camelCase as the standard output format)
58+
writer.WriteString(TypePropertyNameCamelCase, value.Type.ToString());
59+
60+
// Write all other properties from the serialized document
61+
foreach (var property in doc.RootElement.EnumerateObject())
62+
{
63+
property.WriteTo(writer);
64+
}
65+
66+
writer.WriteEndObject();
67+
}
68+
}

src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<TargetFramework>netstandard2.0</TargetFramework>
55
</PropertyGroup>
66

7+
<ItemGroup>
8+
<InternalsVisibleTo Include="Microsoft.ComponentDetection.Contracts.Tests" />
9+
</ItemGroup>
10+
711
<ItemGroup>
812
<PackageReference Include="Microsoft.Extensions.Logging" />
913
<PackageReference Include="Newtonsoft.Json" />

src/Microsoft.ComponentDetection.Contracts/TypedComponent/TypedComponent.cs

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,23 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7-
using System.Text.Json.Serialization;
87
using Microsoft.ComponentDetection.Contracts.BcdeModels;
98
using Newtonsoft.Json;
109
using Newtonsoft.Json.Converters;
1110
using Newtonsoft.Json.Serialization;
1211
using PackageUrl;
1312
using JsonConverterAttribute = Newtonsoft.Json.JsonConverterAttribute;
1413
using JsonIgnoreAttribute = Newtonsoft.Json.JsonIgnoreAttribute;
14+
using SystemTextJson = System.Text.Json.Serialization;
1515

1616
[JsonObject(MemberSerialization.OptOut, NamingStrategyType = typeof(CamelCaseNamingStrategy))]
17-
[JsonConverter(typeof(TypedComponentConverter))]
17+
[JsonConverter(typeof(TypedComponentConverter))] // Newtonsoft.Json
1818
[DebuggerDisplay("{DebuggerDisplay,nq}")]
19-
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
20-
[JsonDerivedType(typeof(CargoComponent), typeDiscriminator: nameof(ComponentType.Cargo))]
21-
[JsonDerivedType(typeof(ConanComponent), typeDiscriminator: nameof(ComponentType.Conan))]
22-
[JsonDerivedType(typeof(CondaComponent), typeDiscriminator: nameof(ComponentType.Conda))]
23-
[JsonDerivedType(typeof(DockerImageComponent), typeDiscriminator: nameof(ComponentType.DockerImage))]
24-
[JsonDerivedType(typeof(DockerReferenceComponent), typeDiscriminator: nameof(ComponentType.DockerReference))]
25-
[JsonDerivedType(typeof(DotNetComponent), typeDiscriminator: nameof(ComponentType.DotNet))]
26-
[JsonDerivedType(typeof(GitComponent), typeDiscriminator: nameof(ComponentType.Git))]
27-
[JsonDerivedType(typeof(GoComponent), typeDiscriminator: nameof(ComponentType.Go))]
28-
[JsonDerivedType(typeof(LinuxComponent), typeDiscriminator: nameof(ComponentType.Linux))]
29-
[JsonDerivedType(typeof(MavenComponent), typeDiscriminator: nameof(ComponentType.Maven))]
30-
[JsonDerivedType(typeof(NpmComponent), typeDiscriminator: nameof(ComponentType.Npm))]
31-
[JsonDerivedType(typeof(NuGetComponent), typeDiscriminator: nameof(ComponentType.NuGet))]
32-
[JsonDerivedType(typeof(OtherComponent), typeDiscriminator: nameof(ComponentType.Other))]
33-
[JsonDerivedType(typeof(PipComponent), typeDiscriminator: nameof(ComponentType.Pip))]
34-
[JsonDerivedType(typeof(PodComponent), typeDiscriminator: nameof(ComponentType.Pod))]
35-
[JsonDerivedType(typeof(RubyGemsComponent), typeDiscriminator: nameof(ComponentType.RubyGems))]
36-
[JsonDerivedType(typeof(SpdxComponent), typeDiscriminator: nameof(ComponentType.Spdx))]
37-
[JsonDerivedType(typeof(SwiftComponent), typeDiscriminator: nameof(ComponentType.Swift))]
38-
[JsonDerivedType(typeof(VcpkgComponent), typeDiscriminator: nameof(ComponentType.Vcpkg))]
19+
[SystemTextJson.JsonConverter(typeof(TypedComponentSystemTextJsonConverter))] // System.Text.Json
3920
public abstract class TypedComponent
4021
{
4122
[JsonIgnore] // Newtonsoft.Json
42-
[System.Text.Json.Serialization.JsonIgnore] // System.Text.Json
23+
[SystemTextJson.JsonIgnore] // System.Text.Json
4324
private string id;
4425

4526
internal TypedComponent()
@@ -50,19 +31,19 @@ internal TypedComponent()
5031
/// <summary>Gets the type of the component, must be well known.</summary>
5132
[JsonConverter(typeof(StringEnumConverter))] // Newtonsoft.Json
5233
[JsonProperty("type", Order = int.MinValue)] // Newtonsoft.Json
53-
[System.Text.Json.Serialization.JsonIgnore] // System.Text.Json - type is handled by [JsonPolymorphic] discriminator
34+
[SystemTextJson.JsonIgnore] // System.Text.Json - type is handled by TypedComponentSystemTextJsonConverter
5435
public abstract ComponentType Type { get; }
5536

5637
/// <summary>Gets the id of the component.</summary>
5738
[JsonProperty("id")] // Newtonsoft.Json
58-
[JsonPropertyName("id")] // System.Text.Json
39+
[SystemTextJson.JsonPropertyName("id")] // System.Text.Json
5940
public string Id => this.id ??= this.ComputeId();
6041

61-
[JsonPropertyName("packageUrl")]
42+
[SystemTextJson.JsonPropertyName("packageUrl")]
6243
public virtual PackageURL PackageUrl { get; }
6344

6445
[JsonIgnore] // Newtonsoft.Json
65-
[System.Text.Json.Serialization.JsonIgnore] // System.Text.Json
46+
[SystemTextJson.JsonIgnore] // System.Text.Json
6647
internal string DebuggerDisplay => $"{this.Id}";
6748

6849
protected string ValidateRequiredInput(string input, string fieldName, string componentType)

src/Microsoft.ComponentDetection.Orchestrator/Services/DetectorProcessingService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,18 @@ public class DetectorProcessingService : IDetectorProcessingService
3131
private readonly IObservableDirectoryWalkerFactory scanner;
3232
private readonly ILogger<DetectorProcessingService> logger;
3333
private readonly IExperimentService experimentService;
34+
private readonly IAnsiConsole console;
3435

3536
public DetectorProcessingService(
3637
IObservableDirectoryWalkerFactory scanner,
3738
IExperimentService experimentService,
38-
ILogger<DetectorProcessingService> logger)
39+
ILogger<DetectorProcessingService> logger,
40+
IAnsiConsole console = null)
3941
{
4042
this.scanner = scanner;
4143
this.experimentService = experimentService;
4244
this.logger = logger;
45+
this.console = console ?? AnsiConsole.Console;
4346
}
4447

4548
/// <inheritdoc/>
@@ -384,7 +387,7 @@ private void LogTabularOutput(ConcurrentDictionary<string, DetectorRunResult> pr
384387
providerElapsedTime.Sum(x => x.Value.ComponentsFoundCount).ToString(),
385388
providerElapsedTime.Sum(x => x.Value.ExplicitlyReferencedComponentCount).ToString());
386389

387-
AnsiConsole.Write(table);
390+
this.console.Write(table);
388391

389392
var tsf = new TabularStringFormat(
390393
[

0 commit comments

Comments
 (0)