Generated the JSON contracts of the stored data at compile time (#1148)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-02 18:23:19 +01:00
1 parent fc7399e81b
commit e954881fda
75 files changed
+5437 -298

No files matched your search

+8 -3
View File
@@ -26,11 +26,16 @@ public sealed class ChatDraft
{
private readonly Lock stateLock = new();
//
// The setters are internal only for reading the JSON, since its source generation reaches no private
// setter. Everything else changes the draft through the methods below, which hold the lock.
//
/// <summary>
/// The text of the message.
/// </summary>
[JsonInclude]
public string Text { get; private set; } = string.Empty;
public string Text { get; internal set; } = string.Empty;
/// <summary>
/// The files attached to the message, the transcripts of audio and video files included.
@@ -40,7 +45,7 @@ public sealed class ChatDraft
/// transcript is still a transcript after the chat was loaded again.
/// </remarks>
[JsonInclude]
public IReadOnlyList<FileAttachment> FileAttachments { get; private set; } = [];
public IReadOnlyList<FileAttachment> FileAttachments { get; internal set; } = [];
/// <summary>
/// The texts a preview read of the attached files, which sending takes along instead of reading the files again.
@@ -53,7 +58,7 @@ public sealed class ChatDraft
/// store holds the texts themselves.
/// </remarks>
[JsonInclude]
public IReadOnlyList<PreviewedText> PreviewedTexts { get; private set; } = [];
public IReadOnlyList<PreviewedText> PreviewedTexts { get; internal set; } = [];
/// <summary>
/// Whether the draft holds neither text nor attachments.
@@ -0,0 +1,17 @@
using System.Text.Json.Serialization;
using ERIContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
namespace AIStudio.Chat;
/// <summary>
/// The contracts with which System.Text.Json reads and writes chats, built at compile time.
/// </summary>
/// <remarks>
/// Two types with the same name need a name of their own for their contract, else the generator builds
/// only the first of them and warns with SYSLIB1031.
/// </remarks>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ChatThread))]
[JsonSerializable(typeof(ERIContentType), TypeInfoPropertyName = "ERIContentType")]
internal sealed partial class ChatJsonContext : JsonSerializerContext;
+8 -8
View File
@@ -23,7 +23,7 @@ public sealed record ChatThread
/// <summary>
/// The unique identifier of the chat thread.
/// </summary>
public Guid ChatId { get; init; }
public Guid ChatId { get; set; }
/// <summary>
/// The unique identifier of the workspace.
@@ -42,16 +42,16 @@ public sealed record ChatThread
/// Every copy of this thread made with a with expression shares the draft, because such a copy is
/// still the same chat. A copy which becomes another chat needs a draft of its own.
/// </remarks>
public ChatDraft Draft { get; init; } = new();
public ChatDraft Draft { get; set; } = new();
// Compatibility shim: until v26.9.1, a chat stored only the transcripts of its draft, in a list of their own. See documentation/compatibility-shims/2026-10-chat-pending-media-transcripts.md; remove after 2027-04-11.
[JsonInclude]
[JsonPropertyName("pending_media_transcripts")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
private List<ManagedTranscriptAttachment>? LegacyPendingMediaTranscripts
internal List<ManagedTranscriptAttachment>? LegacyPendingMediaTranscripts
{
get => null;
init
set
{
if (value is not null)
this.Draft.AddAttachments(value);
@@ -127,7 +127,7 @@ public sealed record ChatThread
/// continues it must meet the level.
/// </summary>
[JsonInclude]
public ConfidenceLevel RequiredProviderConfidence { get; private set; } = ConfidenceLevel.NONE;
public ConfidenceLevel RequiredProviderConfidence { get; internal set; } = ConfidenceLevel.NONE;
public void RequireProviderConfidence(ConfidenceLevel minimumProviderConfidence)
{
@@ -164,7 +164,7 @@ public sealed record ChatThread
/// internal content demands nothing.
/// </remarks>
[JsonInclude]
public OutboundDataRequirement RequiredOutboundDataRestriction { get; private set; } = OutboundDataRequirement.NONE;
public OutboundDataRequirement RequiredOutboundDataRestriction { get; internal set; } = OutboundDataRequirement.NONE;
/// <summary>
/// Tightens where this chat may still send data to what the data brought in demands, and
@@ -229,7 +229,7 @@ public sealed record ChatThread
/// immutable, so that a copy of the thread never shares a set which grows later.
/// </remarks>
[JsonInclude]
public ImmutableHashSet<string> WebAddressesWrittenByModels { get; private set; } = [];
public ImmutableHashSet<string> WebAddressesWrittenByModels { get; internal set; } = [];
/// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
@@ -244,7 +244,7 @@ public sealed record ChatThread
/// <summary>
/// The content blocks of the chat thread.
/// </summary>
public List<ContentBlock> Blocks { get; init; } = [];
public List<ContentBlock> Blocks { get; set; } = [];
/// <summary>
/// The component running this thread, which decides the tools it may use and what its requests name.
+3 -3
View File
@@ -8,12 +8,12 @@ public class ContentBlock
/// <summary>
/// Time when the content block was created.
/// </summary>
public DateTimeOffset Time { get; init; }
public DateTimeOffset Time { get; set; }
/// <summary>
/// Type of the content block, e.g., text, image, voice, etc.
/// </summary>
public ContentType ContentType { get; init; } = ContentType.NONE;
public ContentType ContentType { get; set; } = ContentType.NONE;
/// <summary>
/// The content of the block.
@@ -28,7 +28,7 @@ public class ContentBlock
/// <summary>
/// Should the content block be hidden from the user?
/// </summary>
public bool HideFromUser { get; init; }
public bool HideFromUser { get; set; }
public ContentBlock DeepClone(bool changeHideState = false, bool hideFromUser = true) => new()
{
+1 -1
View File
@@ -58,7 +58,7 @@ public sealed class ContentImage : IContent, IImageSource
/// <remarks>
/// Is the image source a URL, a local file path, a base64 string, etc.?
/// </remarks>
public required ContentImageSource SourceType { get; init; }
public required ContentImageSource SourceType { get; set; }
/// <summary>
/// The image source.
@@ -33,10 +33,10 @@ public sealed class ContentPresentation
}
}
internal static ContentPresentation Create<T>(ContentPresentationKind kind, T data) => new()
internal static ContentPresentation Create<T>(ContentPresentationKind kind, T data, JsonSerializerOptions options) => new()
{
Kind = kind,
Data = JsonSerializer.SerializeToElement(data),
Data = JsonSerializer.SerializeToElement(data, options),
};
internal ContentPresentation DeepClone() => new()
+17 -3
View File
@@ -15,8 +15,22 @@ namespace AIStudio.Chat;
[JsonDerivedType(typeof(FileAttachment), typeDiscriminator: "file")]
[JsonDerivedType(typeof(FileAttachmentImage), typeDiscriminator: "image")]
[JsonDerivedType(typeof(ManagedTranscriptAttachment), typeDiscriminator: "managed_transcript")]
public record FileAttachment(FileAttachmentType Type, string FileName, [property: JsonConverter(typeof(DataDirectoryRelativePathConverter))] string FilePath, long FileSizeBytes)
public record FileAttachment(FileAttachmentType Type, string FileName, string FilePath, long FileSizeBytes)
{
//
// The properties of the parameters, declared again to be settable. A derived attachment gets the type from
// its own constructor, and the source generation of System.Text.Json sets every init-only property while it
// creates the object, also one missing in the JSON, which would lose that type. Keep them first and in the
// order of the parameters, which is the order they get stored in:
//
public FileAttachmentType Type { get; set; } = Type;
public string FileName { get; set; } = FileName;
[JsonConverter(typeof(DataDirectoryRelativePathConverter))]
public string FilePath { get; set; } = FilePath;
public long FileSizeBytes { get; set; } = FileSizeBytes;
/// <summary>
/// Gets a value indicating whether the file type is forbidden and should not be attached.
/// </summary>
@@ -69,7 +83,7 @@ public record FileAttachment(FileAttachmentType Type, string FileName, [property
/// of chat templates, while null.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Guid? ProvenanceId { get; init; }
public Guid? ProvenanceId { get; set; }
/// <summary>
/// The state of the file which the stored text was read from, see ProvenanceId, or null while
@@ -80,7 +94,7 @@ public record FileAttachment(FileAttachmentType Type, string FileName, [property
/// DataChat.ReadChangedAttachmentsAgain. Left out of the chat file while null.
/// </remarks>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public FileVersion? Version { get; init; }
public FileVersion? Version { get; set; }
/// <summary>
/// Whether the file changed since its text was stored.
@@ -10,7 +10,7 @@ public record FileAttachmentImage(string FileName, string FilePath, long FileSiz
/// <remarks>
/// Is the image source a URL, a local file path, a base64 string, etc.?
/// </remarks>
public ContentImageSource SourceType { get; init; } = ContentImageSource.LOCAL_PATH;
public ContentImageSource SourceType { get; set; } = ContentImageSource.LOCAL_PATH;
/// <summary>
/// The image source.
+1 -1
View File
@@ -8,7 +8,7 @@ public interface IImageSource
/// <remarks>
/// Is the image source a URL, a local file path, a base64 string, etc.?
/// </remarks>
public ContentImageSource SourceType { get; init; }
public ContentImageSource SourceType { get; set; }
/// <summary>
/// The image source.
@@ -20,7 +20,7 @@ public sealed record ReportedTokenUsage
/// <summary>
/// What everything sent to the model cost.
/// </summary>
public int PromptTokens { get; init; }
public int PromptTokens { get; set; }
/// <summary>
/// Which model the number was charged for.
@@ -31,7 +31,7 @@ public sealed record ReportedTokenUsage
/// being an answer about the request which is about to be sent, and the estimate, wrong as it
/// is, is at least wrong about the right model.
/// </remarks>
public string ModelId { get; init; } = string.Empty;
public string ModelId { get; set; } = string.Empty;
/// <summary>
/// How many blocks the conversation had when the request went out, this answer included.
@@ -45,7 +45,7 @@ public sealed record ReportedTokenUsage
/// Taken when the request is sent rather than when the report arrives: whatever is deleted
/// while the answer streams in was still part of what the provider counted.
/// </remarks>
public int BlockCount { get; init; }
public int BlockCount { get; set; }
/// <summary>
/// States the stored number as a usage again.