![]() |
VOOZH | about |
dotnet add package RedMist.TimingCommon --version 1.5.0
NuGet\Install-Package RedMist.TimingCommon -Version 1.5.0
<PackageReference Include="RedMist.TimingCommon" Version="1.5.0" />
<PackageVersion Include="RedMist.TimingCommon" Version="1.5.0" />Directory.Packages.props
<PackageReference Include="RedMist.TimingCommon" />Project file
paket add RedMist.TimingCommon --version 1.5.0
#r "nuget: RedMist.TimingCommon, 1.5.0"
#:package RedMist.TimingCommon@1.5.0
#addin nuget:?package=RedMist.TimingCommon&version=1.5.0Install as a Cake Addin
#tool nuget:?package=RedMist.TimingCommon&version=1.5.0Install as a Cake Tool
This system provides automatic generation of patch variants for classes, enabling efficient partial updates over network protocols like SignalR with minimal payload sizes. The system now automatically generates both patch classes and Mapperly mappers.
The patch generation system consists of:
[GeneratePatch] Attribute - Marks classes for patch generation[GeneratePatch]
[MessagePackObject]
public class SessionState
{
[MessagePack.Key(0)]
public int EventId { get; set; }
[MessagePack.Key(1)]
[MaxLength(512)]
public string EventName { get; set; } = string.Empty;
[MessagePack.Key(2)]
public bool IsLive { get; set; }
// ... more properties
}
The generator automatically creates SessionStatePatch:
[MessagePackObject]
public class SessionStatePatch
{
[MessagePack.Key(0)]
public int? EventId { get; set; }
[MessagePack.Key(1)]
[MaxLength(512)]
public string? EventName { get; set; }
[MessagePack.Key(2)]
public bool? IsLive { get; set; }
// ... all properties become nullable
}
The generator also creates SessionStateMapper with full functionality:
[Mapper]
public static partial class SessionStateMapper
{
// Auto-generated by Mapperly
public static partial void ApplyPatch(SessionStatePatch patch, SessionState target);
// Auto-generated by Mapperly
public static partial SessionState PatchToEntity(SessionStatePatch patch);
// Custom diff creation (generated by PatchClassGenerator)
public static SessionStatePatch CreatePatch(SessionState original, SessionState updated);
// Helper methods (generated by PatchClassGenerator)
public static bool IsValidPatch(SessionStatePatch patch);
public static string[] GetChangedProperties(SessionStatePatch patch);
}
// Original state
var sessionState = new SessionState
{
EventId = 1,
EventName = "Test Race",
IsLive = false
};
// Create patch with only changed fields
var patch = new SessionStatePatch
{
IsLive = true, // Only this changed
// EventId and EventName remain null = no change
};
// Apply patch efficiently using auto-generated mapper
SessionStateMapper.ApplyPatch(patch, sessionState);
// Validate and inspect patches
bool isValid = SessionStateMapper.IsValidPatch(patch);
string[] changedProps = SessionStateMapper.GetChangedProperties(patch);
// Server side - send minimal updates
public async Task SendSessionUpdate(SessionState newState)
{
var patch = SessionStateMapper.CreatePatch(_previousState, newState);
// Only send if there are actual changes
if (SessionStateMapper.IsValidPatch(patch))
{
await Clients.All.SendAsync("SessionStateUpdate", patch);
}
_previousState = newState;
}
// Client side - apply updates
public async Task ReceiveSessionUpdate(SessionStatePatch patch)
{
SessionStateMapper.ApplyPatch(patch, _localSessionState);
// Optional: Log what changed
var changedProps = SessionStateMapper.GetChangedProperties(patch);
Console.WriteLine($"Updated properties: {string.Join(", ", changedProps)}");
await UpdateUI();
}
[GeneratePatch(
PatchClassName = "SessionDelta",
PatchNamespace = "MyApp.Deltas",
MapperClassName = "SessionDeltaMapper",
MapperNamespace = "MyApp.Deltas.Mappers",
IncludeMessagePackAttributes = true,
IncludeJsonAttributes = false,
IncludeValidationAttributes = true,
GenerateMapper = true
)]
public class CustomExample
{
[MessagePack.Key(0)]
public int Id { get; set; }
[MessagePack.Key(1)]
public string Name { get; set; } = string.Empty;
}
PatchClassName - Custom name for generated patch class (default: {ClassName}Patch)PatchNamespace - Custom namespace for patch class (default: same as source class)MapperClassName - Custom name for generated mapper class (default: {ClassName}Mapper)MapperNamespace - Custom namespace for mapper class (default: {SourceNamespace}.Mappers)IncludeMessagePackAttributes - Include MessagePack serialization attributes (default: true)IncludeJsonAttributes - Include System.Text.Json attributes (default: true)IncludeValidationAttributes - Include validation attributes like [MaxLength] (default: true)GenerateMapper - Whether to generate the Mapperly mapper class (default: true)[GeneratePatch(GenerateMapper = false)]
public class PatchOnlyExample
{
public int Value { get; set; }
public string Description { get; set; } = string.Empty;
}
// Generates: PatchOnlyExamplePatch class only, no mapper
Each auto-generated mapper includes:
ApplyPatch(patch, target) - Applies patch to existing object (generated by Mapperly)PatchToEntity(patch) - Creates new object from patch (generated by Mapperly)CreatePatch(original, updated) - Creates patch with only differencesIsValidPatch(patch) - Checks if patch has any changesGetChangedProperties(patch) - Returns array of changed property namespublic static void PerformanceComparison()
{
var largeState = CreateLargeSessionState(); // 50 cars, full data
var updatedState = largeState; // Copy
updatedState.LapsToGo = largeState.LapsToGo - 1; // Small change
// Full object serialization
var fullBytes = MessagePackSerializer.Serialize(updatedState);
Console.WriteLine($"Full object: {fullBytes.Length} bytes");
// Patch serialization
var patch = SessionStateMapper.CreatePatch(largeState, updatedState);
var patchBytes = MessagePackSerializer.Serialize(patch);
Console.WriteLine($"Patch: {patchBytes.Length} bytes");
// Typical reduction: 90%+ for small changes
var reduction = (1.0 - (double)patchBytes.Length / fullBytes.Length) * 100;
Console.WriteLine($"Size reduction: {reduction:F1}%");
}
<PackageReference Include="MessagePack" Version="2.5.140" />
<PackageReference Include="Riok.Mapperly" Version="4.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<ItemGroup>
<Compile Remove="Generators\**" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="Generators\PatchClassGenerator.cs" />
</ItemGroup>
After marking classes with [GeneratePatch], build the project to generate:
{ClassName}Patch.g.cs - The patch class{ClassName}Mapper.g.cs - The Mapperly mapper (if GenerateMapper = true)IsValidPatch)GetChangedProperties)Ideal for objects that:
[GeneratePatch]
[MessagePackObject]
public class EvolvingClass
{
[MessagePack.Key(0)] public int Id { get; set; }
[MessagePack.Key(1)] public string Name { get; set; } = "";
// When adding new properties, use next available key
[MessagePack.Key(2)] public DateTime CreatedAt { get; set; }
// Never reuse or change existing key numbers for compatibility
}
// Good - batch related changes in one patch
var patch = new SessionStatePatch
{
LapsToGo = 5,
TimeToGo = "00:15:30",
CurrentFlag = Flags.Yellow
};
// Avoid - multiple individual patches for related changes
public async Task SendUpdate<T>(T patch) where T : class
{
// Only send if patch contains changes
if (IsValidPatch(patch))
{
await hubContext.Clients.All.SendAsync("Update", patch);
}
}
// Server
services.AddSignalR()
.AddMessagePackProtocol();
// Client
connection = new HubConnectionBuilder()
.WithUrl("/timingHub")
.AddMessagePackProtocol()
.Build();
public class TimingHub : Hub
{
public async Task SubscribeToSessionUpdates(int eventId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"Event_{eventId}");
}
}
// Background service
public class TimingService
{
private SessionState _lastState = new();
public async Task BroadcastUpdate(SessionState newState, int eventId)
{
var patch = SessionStateMapper.CreatePatch(_lastState, newState);
if (SessionStateMapper.IsValidPatch(patch))
{
await _hubContext.Clients.Group($"Event_{eventId}")
.SendAsync("SessionStateUpdate", patch);
// Log what changed for debugging
var changes = SessionStateMapper.GetChangedProperties(patch);
_logger.LogDebug("Updated properties: {Properties}", string.Join(", ", changes));
}
_lastState = newState;
}
}
Generator Not Running
<Analyzer Include="..."> is correctly configuredMapperly Compilation Errors
MessagePack Serialization Issues
[MessagePack.Key] attributes have unique indicesPatch Not Generated
[GeneratePatch] attribute is appliedpublic and has public propertiesGenerated files are available in your project under:
obj/Debug/net9.0/generated/RedMist.TimingCommon.Generators.PatchClassGenerator/
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.5.0 | 42 | 6/18/2026 |
| 1.4.0 | 218 | 5/24/2026 |
| 1.3.9 | 535 | 3/15/2026 |
| 1.3.8 | 151 | 3/14/2026 |
| 1.3.7 | 145 | 3/9/2026 |
| 1.3.6 | 346 | 2/20/2026 |
| 1.3.5 | 213 | 2/8/2026 |
| 1.3.4 | 234 | 2/6/2026 |
| 1.3.3 | 330 | 1/22/2026 |
| 1.3.2 | 191 | 1/3/2026 |
| 1.3.1 | 162 | 12/27/2025 |
| 1.3.0 | 208 | 12/25/2025 |
| 1.2.8 | 252 | 12/13/2025 |
| 1.2.7 | 276 | 11/30/2025 |
| 1.2.6 | 138 | 11/29/2025 |
| 1.2.5 | 317 | 11/11/2025 |
| 1.2.4 | 312 | 11/11/2025 |
| 1.2.3 | 165 | 11/7/2025 |
| 1.2.2 | 212 | 11/3/2025 |
| 1.2.1 | 217 | 11/2/2025 |