Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add source-generated JSON serialization for NativeAOT
NativeAOT cannot use runtime reflection for JSON serialization.
GVFSJsonContext provides source-generated System.Text.Json serializers
for 25+ types used in named pipe messages and configuration.

GVFSJsonOptions chains source-gen (primary) with reflection fallback
for types not yet in the context, allowing incremental migration.

NamedPipeMessages: add parameterless constructors required by the
source generator's deserialization codegen.

RepoRegistration: add ServiceJsonContext source generator in
GVFS.Service for types that cannot be registered in GVFSJsonContext
(GVFS.Common) due to assembly dependency direction.

Co-authored-by: Michael Niksa <miniksa@microsoft.com>
Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
  • Loading branch information
tyrielv and miniksa committed May 4, 2026
commit 33d3b952f1b38514dd4274438200e2d0ae92cfc9
47 changes: 47 additions & 0 deletions GVFS/GVFS.Common/GVFSJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using GVFS.Common.Http;
using GVFS.Common.NamedPipes;
using GVFS.Common.Tracing;
using GVFS.Common.Prefetch;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace GVFS.Common
{
/// <summary>
/// Source-generated JSON serializer context for all types used in GVFS serialization.
/// This enables trim-safe and AOT-compatible JSON serialization without reflection.
/// </summary>
[JsonSourceGenerationOptions(
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = new[] { typeof(VersionConverter) })]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(KeyValuePair<string, string>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<GitObjectsHttpRequestor.GitObjectSize>))]
[JsonSerializable(typeof(ServerGVFSConfig))]
[JsonSerializable(typeof(VersionResponse))]
[JsonSerializable(typeof(InternalVerbParameters))]
[JsonSerializable(typeof(CacheServerInfo))]
[JsonSerializable(typeof(NamedPipeMessages.GetStatus.Response), TypeInfoPropertyName = "GetStatusResponse")]
[JsonSerializable(typeof(NamedPipeMessages.DehydrateFolders.Request), TypeInfoPropertyName = "DehydrateFoldersRequest")]
[JsonSerializable(typeof(NamedPipeMessages.DehydrateFolders.Response), TypeInfoPropertyName = "DehydrateFoldersResponse")]
[JsonSerializable(typeof(NamedPipeMessages.Notification.Request), TypeInfoPropertyName = "NotificationRequest")]
[JsonSerializable(typeof(NamedPipeMessages.UnregisterRepoRequest))]
[JsonSerializable(typeof(NamedPipeMessages.UnregisterRepoRequest.Response), TypeInfoPropertyName = "UnregisterRepoResponse")]
[JsonSerializable(typeof(NamedPipeMessages.RegisterRepoRequest))]
[JsonSerializable(typeof(NamedPipeMessages.RegisterRepoRequest.Response), TypeInfoPropertyName = "RegisterRepoResponse")]
[JsonSerializable(typeof(NamedPipeMessages.EnableAndAttachProjFSRequest))]
[JsonSerializable(typeof(NamedPipeMessages.EnableAndAttachProjFSRequest.Response), TypeInfoPropertyName = "EnableAndAttachProjFSResponse")]
[JsonSerializable(typeof(NamedPipeMessages.GetActiveRepoListRequest))]
[JsonSerializable(typeof(NamedPipeMessages.GetActiveRepoListRequest.Response), TypeInfoPropertyName = "GetActiveRepoListResponse")]
[JsonSerializable(typeof(NamedPipeMessages.BaseResponse<string>))]
[JsonSerializable(typeof(TelemetryDaemonEventListener.PipeMessage))]
[JsonSerializable(typeof(PrettyConsoleEventListener.ConsoleOutputPayload))]
internal partial class GVFSJsonContext : JsonSerializerContext
{
}
}
41 changes: 28 additions & 13 deletions GVFS/GVFS.Common/GVFSJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
using GVFS.Common.Tracing;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace GVFS.Common
{
/// <summary>
/// Shared JsonSerializerOptions and helpers for the GVFS codebase.
/// PropertyNameCaseInsensitive preserves backward compatibility with
/// Newtonsoft.Json's default case-insensitive deserialization.
/// Shared JsonSerializerOptions for the GVFS codebase.
/// Uses source-generated GVFSJsonContext for known types (trim-safe/AOT-safe)
/// with DefaultJsonTypeInfoResolver as fallback for types not in the context
/// (e.g., boxed primitives in EventMetadata Dictionary&lt;string, object&gt;).
/// EventMetadata uses a custom converter that handles Dictionary&lt;string, object&gt;
/// without reflection, making it NativeAOT compatible.
/// </summary>
public static class GVFSJsonOptions
{
[UnconditionalSuppressMessage("AOT", "IL2026",
Comment thread
tyrielv marked this conversation as resolved.
Justification = "Uses source-gen context for known types; EventMetadataConverter handles Dictionary<string,object> without reflection. DefaultJsonTypeInfoResolver fallback handles boxed primitives in EventMetadata.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "Uses source-gen context for known types; EventMetadataConverter handles Dictionary<string,object> without reflection. DefaultJsonTypeInfoResolver fallback handles boxed primitives in EventMetadata.")]
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters = { new VersionConverter(), new Tracing.EventMetadataConverter() },
Converters = { new VersionConverter(), new EventMetadataConverter() },
TypeInfoResolverChain = { GVFSJsonContext.Default, new DefaultJsonTypeInfoResolver() },
};

/// <summary>
/// Serialize using the compile-time type. Use when <typeparamref name="T"/>
/// is the concrete type (not a base class with derived properties).
/// </summary>
[UnconditionalSuppressMessage("AOT", "IL2026",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
public static string Serialize<T>(T value)
{
return JsonSerializer.Serialize(value, Default);
}

/// <summary>
/// Serialize using the runtime type. Use when calling from a base-class
/// method where compile-time type would lose derived-class properties
/// (e.g., BaseResponse&lt;T&gt;.ToMessage()).
/// </summary>
[UnconditionalSuppressMessage("AOT", "IL2026",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
public static string Serialize(object value, Type inputType)
{
return JsonSerializer.Serialize(value, inputType, Default);
}

[UnconditionalSuppressMessage("AOT", "IL2026",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
[UnconditionalSuppressMessage("AOT", "IL3050",
Justification = "TypeInfoResolverChain includes GVFSJsonContext (source-gen) + DefaultJsonTypeInfoResolver fallback.")]
public static T Deserialize<T>(string json)
{
return JsonSerializer.Deserialize<T>(json, Default);
Expand Down
18 changes: 13 additions & 5 deletions GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ public Response(string result, string data = "")
this.Data = data;
}

public string Result { get; }
public string Data { get; }
public Response() { }

public string Result { get; set; }
public string Data { get; set; }

public Message CreateMessage()
{
Expand Down Expand Up @@ -129,7 +131,9 @@ public Response(string result)
this.Result = result;
}

public string Result { get; }
public Response() { }

public string Result { get; set; }

public Message CreateMessage()
{
Expand Down Expand Up @@ -185,7 +189,9 @@ public Response(string result)
this.Result = result;
}

public string Result { get; }
public Response() { }

public string Result { get; set; }

public Message CreateMessage()
{
Expand Down Expand Up @@ -296,7 +302,9 @@ public Response(string result)
this.Result = result;
}

public string Result { get; }
public Response() { }

public string Result { get; set; }

public Message CreateMessage()
{
Expand Down
2 changes: 1 addition & 1 deletion GVFS/GVFS.Common/Tracing/PrettyConsoleEventListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ protected override void RecordMessageInternal(TraceEventMessage message)
}
}

private class ConsoleOutputPayload
internal class ConsoleOutputPayload
{
public string ErrorMessage { get; set; }
}
Expand Down
10 changes: 7 additions & 3 deletions GVFS/GVFS.Service/RepoRegistration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GVFS.Common;
using System.Text.Json;

namespace GVFS.Service
{
Expand All @@ -19,9 +19,13 @@ public RepoRegistration(string enlistmentRoot, string ownerSID)
public string OwnerSID { get; set; }
public bool IsActive { get; set; }

// Uses ServiceJsonContext (assembly-local source generator) instead of
// GVFSJsonOptions because RepoRegistration cannot be registered in
// GVFSJsonContext (GVFS.Common) — wrong assembly direction. The
// reflection fallback in GVFSJsonOptions fails under native AOT trimming.
public static RepoRegistration FromJson(string json)
{
return GVFSJsonOptions.Deserialize<RepoRegistration>(json);
return JsonSerializer.Deserialize(json, ServiceJsonContext.Default.RepoRegistration);
}

public override string ToString()
Expand All @@ -36,7 +40,7 @@ public override string ToString()

public string ToJson()
{
return GVFSJsonOptions.Serialize(this);
return JsonSerializer.Serialize(this, ServiceJsonContext.Default.RepoRegistration);
}
}
}
15 changes: 15 additions & 0 deletions GVFS/GVFS.Service/ServiceJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace GVFS.Service
{
/// <summary>
/// Source-generated JSON context for GVFS.Service types that cannot be registered
/// in GVFSJsonContext (GVFS.Common) due to assembly dependency direction.
/// Required for native AOT where the DefaultJsonTypeInfoResolver reflection
/// fallback is not available.
/// </summary>
[JsonSerializable(typeof(RepoRegistration))]
internal partial class ServiceJsonContext : JsonSerializerContext
{
}
}
4 changes: 2 additions & 2 deletions GVFS/GVFS.Virtualization/Background/FileSystemTask.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GVFS.Common;
using System.Text.Json;

namespace GVFS.Virtualization.Background
{
Expand Down Expand Up @@ -133,7 +133,7 @@ public static FileSystemTask OnPlaceholderCreationsBlockedForGit()

public override string ToString()
{
return GVFSJsonOptions.Serialize(this);
return JsonSerializer.Serialize(this, VirtualizationJsonContext.Default.FileSystemTask);
}
}
}
10 changes: 10 additions & 0 deletions GVFS/GVFS.Virtualization/VirtualizationJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
using GVFS.Virtualization.Background;

namespace GVFS.Virtualization
{
[JsonSerializable(typeof(FileSystemTask))]
internal partial class VirtualizationJsonContext : JsonSerializerContext
{
}
}