RedMist.TimingCommon
1.3.9
dotnet add package RedMist.TimingCommon --version 1.3.9
NuGet\Install-Package RedMist.TimingCommon -Version 1.3.9
<PackageReference Include="RedMist.TimingCommon" Version="1.3.9" />
<PackageVersion Include="RedMist.TimingCommon" Version="1.3.9" />
<PackageReference Include="RedMist.TimingCommon" />
paket add RedMist.TimingCommon --version 1.3.9
#r "nuget: RedMist.TimingCommon, 1.3.9"
#:package RedMist.TimingCommon@1.3.9
#addin nuget:?package=RedMist.TimingCommon&version=1.3.9
#tool nuget:?package=RedMist.TimingCommon&version=1.3.9
Patch Class Generation System
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.
Overview
The patch generation system consists of:
[GeneratePatch]Attribute - Marks classes for patch generation- PatchClassGenerator - Source generator that creates patch variants and mappers
- Auto-Generated Mapperly Mappers - Efficient patch application using compile-time mapping
- MessagePack Support - Optimized serialization for network transport
Basic Usage
1. Mark a Class 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
}
2. Auto-Generated Patch Class
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
}
3. Auto-Generated Mapperly Mapper
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);
}
4. Usage Examples
Creating and Applying Patches
// 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);
SignalR Integration
// 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();
}
Advanced Configuration
Full Configuration Options
[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;
}
Attribute Options
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)
Patch-Only Generation
[GeneratePatch(GenerateMapper = false)]
public class PatchOnlyExample
{
public int Value { get; set; }
public string Description { get; set; } = string.Empty;
}
// Generates: PatchOnlyExamplePatch class only, no mapper
Generated Mapper Methods
Each auto-generated mapper includes:
Core Mapping Methods
ApplyPatch(patch, target)- Applies patch to existing object (generated by Mapperly)PatchToEntity(patch)- Creates new object from patch (generated by Mapperly)
Utility Methods (Generated by PatchClassGenerator)
CreatePatch(original, updated)- Creates patch with only differencesIsValidPatch(patch)- Checks if patch has any changesGetChangedProperties(patch)- Returns array of changed property names
Performance Benefits
Payload Size Reduction
public 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}%");
}
Project Setup
1. Add Required Packages
<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" />
2. Configure Source Generator
<ItemGroup>
<Compile Remove="Generators\**" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="Generators\PatchClassGenerator.cs" />
</ItemGroup>
3. Build Project
After marking classes with [GeneratePatch], build the project to generate:
{ClassName}Patch.g.cs- The patch class{ClassName}Mapper.g.cs- The Mapperly mapper (ifGenerateMapper = true)
Benefits
1. Minimal Network Payload
- Only changed fields are serialized and transmitted
- Typically 80-95% reduction in payload size for small changes
- Ideal for real-time applications with frequent updates
2. Zero Boilerplate
- No manual patch class creation
- No manual mapper implementation
- Automatic synchronization with source class changes
3. Type Safety & Performance
- Generated patch classes maintain full type safety
- Mapperly generates optimized mapping code (no reflection)
- MessagePack provides efficient binary serialization
- IntelliSense support for all generated classes
4. Rich Functionality
- Built-in validation (
IsValidPatch) - Change tracking (
GetChangedProperties) - Flexible configuration options
- Seamless Mapperly integration
Best Practices
1. Use for Frequently Updated Objects
Ideal for objects that:
- Change frequently (live race data, real-time metrics)
- Are transmitted over networks regularly
- Have many properties but few change at once
- Need efficient bandwidth usage
2. MessagePack Key Management
[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
}
3. Batch Related Changes
// 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
4. Validate Before Sending
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);
}
}
Integration with SignalR
1. Configure MessagePack Protocol
// Server
services.AddSignalR()
.AddMessagePackProtocol();
// Client
connection = new HubConnectionBuilder()
.WithUrl("/timingHub")
.AddMessagePackProtocol()
.Build();
2. Hub Implementation
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;
}
}
Troubleshooting
Common Issues
Generator Not Running
- Clean and rebuild the project
- Ensure
<Analyzer Include="...">is correctly configured - Check Visual Studio output for generator errors
Mapperly Compilation Errors
- Ensure patch class exists before mapper generation
- Check that all referenced types are accessible
- Verify Mapperly package is installed
MessagePack Serialization Issues
- Verify all
[MessagePack.Key]attributes have unique indices - Ensure referenced types are MessagePack-serializable
- Check for circular references in object graphs
- Verify all
Patch Not Generated
- Verify
[GeneratePatch]attribute is applied - Check that class is
publicand has public properties - Ensure class is not generic or abstract
- Verify
Debug Generated Code
Generated 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 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MessagePack (>= 3.1.4)
- Riok.Mapperly (>= 4.3.1)
- TypeGen (>= 7.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.3.9 | 31 | 3/15/2026 |
| 1.3.8 | 37 | 3/14/2026 |
| 1.3.7 | 115 | 3/9/2026 |
| 1.3.6 | 325 | 2/20/2026 |
| 1.3.5 | 190 | 2/8/2026 |
| 1.3.4 | 213 | 2/6/2026 |
| 1.3.3 | 308 | 1/22/2026 |
| 1.3.2 | 170 | 1/3/2026 |
| 1.3.1 | 144 | 12/27/2025 |
| 1.3.0 | 189 | 12/25/2025 |
| 1.2.8 | 234 | 12/13/2025 |
| 1.2.7 | 257 | 11/30/2025 |
| 1.2.6 | 120 | 11/29/2025 |
| 1.2.5 | 298 | 11/11/2025 |
| 1.2.4 | 294 | 11/11/2025 |
| 1.2.3 | 146 | 11/7/2025 |
| 1.2.2 | 195 | 11/3/2025 |
| 1.2.1 | 199 | 11/2/2025 |
| 1.2.0 | 195 | 11/2/2025 |
| 1.1.6 | 308 | 10/24/2025 |