Helpers and Source Generation¶
What Are Source Generators¶
Source generators are a C# feature (introduced in C# 9.0) that automatically write code for you at compile time. Think of them as smart code wizards that look at your code, see what you need, and generate the boilerplate automatically.
In plain English¶
- You write:
[DxAutoConstructor]on a struct - Source generator sees: "Oh, they want a constructor!"
- Source generator creates: A constructor with all the fields as parameters
- You get: Less typing, fewer bugs, more consistent code
Learn more about source generators¶
DxMessaging Attributes (Your Code Wizards)¶
DxMessaging provides 3 main attributes that use source generators to eliminate boilerplate:
1. Message Type Attributes (Pick Your Message Category)¶
These tell the source generator what KIND of message you're making:
[DxUntargetedMessage] - Global Messages¶
[DxUntargetedMessage] // ← Tells generator: "This is a global message"
public readonly partial struct GamePaused { }
What it generates¶
- Implements
IUntargetedMessage<GamePaused> - Adds required plumbing for the message system
- Makes it work with
.Emit()extension methods
[DxTargetedMessage] - Messages to Specific Targets¶
[DxTargetedMessage] // ← Tells generator: "This goes to one specific target"
public readonly partial struct Heal {
public readonly int amount;
}
What it generates¶
- Implements
ITargetedMessage<Heal> - Adds required plumbing for targeted emissions
- Makes it work with
.EmitGameObjectTargeted()/.EmitComponentTargeted()
[DxBroadcastMessage] - Messages from a Source¶
[DxBroadcastMessage] // ← Tells generator: "This broadcasts from a source"
public readonly partial struct TookDamage {
public readonly int amount;
}
What it generates¶
- Implements
IBroadcastMessage<TookDamage> - Adds required plumbing for broadcast emissions
- Makes it work with
.EmitGameObjectBroadcast()/.EmitComponentBroadcast()
2. [DxAutoConstructor] - Automatic Constructors¶
Problem: Writing constructors for every message is tedious and error-prone.
Solution: Let the source generator do it!
[DxUntargetedMessage]
[DxAutoConstructor] // ← Magic happens here!
public readonly partial struct VideoSettingsChanged
{
public readonly int width;
public readonly int height;
}
What you get (auto-generated)¶
// You don't write this - it's generated for you!
public VideoSettingsChanged(int width, int height)
{
this.width = width;
this.height = height;
}
Rules¶
- Creates constructor parameters in field declaration order
- Only includes
publicfields - Ignores
staticfields - Works with
readonlystructs (recommended!) - Supports nested types (types defined inside other classes/structs)
- Supports internal types (types with
internalvisibility)
3. [DxOptionalParameter] - Optional Constructor Parameters¶
The DxOptionalParameterAttribute marks a field as optional when used with [DxAutoConstructor]. The source generator emits a constructor parameter with a default value for annotated fields, making it easy to create messages with sensible defaults.
How It Works with DxAutoConstructor¶
When you combine [DxOptionalParameter] with [DxAutoConstructor], the generator creates a constructor where:
- Required fields (without the attribute) become regular constructor parameters
- Optional fields (with the attribute) become parameters with default values
- Parameter order follows field declaration order
- C# rules apply - optional parameters must come after required parameters
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct SettingsChanged
{
public readonly float volume; // Required (no attribute)
public readonly int quality; // Required (no attribute)
[DxOptionalParameter] // Optional, defaults to false
public readonly bool fullscreen;
[DxOptionalParameter(100)] // Optional, defaults to 100
public readonly int brightness;
}
Generated constructor¶
public SettingsChanged(float volume, int quality, bool fullscreen = default, int brightness = 100)
{
this.volume = volume;
this.quality = quality;
this.fullscreen = fullscreen;
this.brightness = brightness;
}
Calling the Constructor¶
Optional parameters support all standard C# calling conventions:
// Provide all arguments
var settings1 = new SettingsChanged(0.8f, 2, true, 80);
// Use all defaults
var settings2 = new SettingsChanged(0.8f, 2); // fullscreen=false, brightness=100
// Use named arguments to skip some optionals
var settings3 = new SettingsChanged(0.8f, 2, fullscreen: true); // brightness=100
var settings4 = new SettingsChanged(0.8f, 2, brightness: 50); // fullscreen=false
Supported Default Value Types¶
The attribute provides constructor overloads for all common primitive types:
ℹ️ Info: Built-in Type Support
Numeric:
Category Types Signed Integers sbyte,short,int,longUnsigned Integers byte,ushort,uint,ulongFloating Point float,doubleOther:
Category Types Boolean boolCharacter charString stringFor enums, Unity types, and other complex types, use the
Expressionproperty.
Examples by type¶
[DxAutoConstructor]
public readonly partial struct TypeExamples
{
// Boolean
[DxOptionalParameter(true)]
public readonly bool isEnabled;
// Character
[DxOptionalParameter('X')]
public readonly char grade;
// String
[DxOptionalParameter("unknown")]
public readonly string playerName;
// Signed integers
[DxOptionalParameter((sbyte)-1)]
public readonly sbyte signedByte;
[DxOptionalParameter((short)100)]
public readonly short shortValue;
[DxOptionalParameter(42)]
public readonly int score;
[DxOptionalParameter(9999999999L)]
public readonly long bigNumber;
// Unsigned integers
[DxOptionalParameter((byte)255)]
public readonly byte level;
[DxOptionalParameter((ushort)1000)]
public readonly ushort maxHealth;
[DxOptionalParameter(50u)]
public readonly uint count;
[DxOptionalParameter(100UL)]
public readonly ulong totalPoints;
// Floating point
[DxOptionalParameter(1.5f)]
public readonly float speed;
[DxOptionalParameter(3.14159)]
public readonly double precision;
}
Using Type Defaults¶
When you use [DxOptionalParameter] without a value, the field defaults to the type's default value:
[DxAutoConstructor]
public readonly partial struct DefaultExamples
{
[DxOptionalParameter] // Defaults to 0
public readonly int count;
[DxOptionalParameter] // Defaults to false
public readonly bool isActive;
[DxOptionalParameter] // Defaults to null
public readonly string name;
[DxOptionalParameter] // Defaults to 0.0f
public readonly float multiplier;
}
Advanced: Custom Expressions for Any Type¶
For types that don't have built-in constructor overloads (enums, nullables, Unity types, custom structs), use the Expression property. The expression is inserted verbatim into the generated constructor, and the C# compiler enforces type safety:
[DxAutoConstructor]
public readonly partial struct AdvancedDefaults
{
// Nullable types
[DxOptionalParameter(Expression = "null")]
public readonly int? optionalId;
// Enum values
[DxOptionalParameter(Expression = "DamageType.Physical")]
public readonly DamageType damageType;
// Unity types
[DxOptionalParameter(Expression = "Vector3.zero")]
public readonly Vector3 position;
[DxOptionalParameter(Expression = "Color.white")]
public readonly Color tint;
// Static constants
[DxOptionalParameter(Expression = "float.MaxValue")]
public readonly float range;
// Named constants from your code
[DxOptionalParameter(Expression = "GameConstants.DefaultHealth")]
public readonly int health;
}
💡 Tip: Expression rules
- The expression is inserted verbatim into the generated code
- Must be a valid C# constant expression
- Type safety is enforced by the C# compiler at build time
- Perfect for enums, nullable types, static constants, or complex defaults
Code Examples¶
Example 1: Basic Optional Parameter with Default Value¶
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal
{
public readonly int amount;
[DxOptionalParameter(false)]
public readonly bool isCritical;
}
// Usage:
var normalHeal = new Heal(25); // isCritical = false
var critHeal = new Heal(50, true); // isCritical = true
Example 2: Multiple Optional Parameters¶
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct SpawnEffect
{
[DxOptionalParameter(1.0f)]
public readonly float scale;
[DxOptionalParameter(Expression = "Color.white")]
public readonly Color color;
[DxOptionalParameter(5.0f)]
public readonly float duration;
[DxOptionalParameter(false)]
public readonly bool looping;
}
// Usage:
var defaultEffect = new SpawnEffect(); // All defaults
var largeEffect = new SpawnEffect(2.5f); // Custom scale
var redEffect = new SpawnEffect(1.0f, Color.red); // Custom color
var customEffect = new SpawnEffect(1.5f, Color.blue, 10f, true); // All custom
Example 3: Mix of Required and Optional Fields¶
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct GameEvent
{
// Required fields (must be provided)
public readonly string eventName;
public readonly int playerId;
// Optional fields (have defaults)
[DxOptionalParameter(0)]
public readonly int score;
[DxOptionalParameter("")]
public readonly string metadata;
[DxOptionalParameter(Expression = "System.DateTime.UtcNow")]
public readonly System.DateTime timestamp;
}
// Usage:
var minimalEvent = new GameEvent("LevelStart", 1);
var scoredEvent = new GameEvent("EnemyKilled", 1, 100);
var fullEvent = new GameEvent("Achievement", 1, 500, "first_blood");
Example 4: Game Settings Message¶
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct AudioSettingsChanged
{
[DxOptionalParameter(1.0f)]
public readonly float masterVolume;
[DxOptionalParameter(0.8f)]
public readonly float musicVolume;
[DxOptionalParameter(1.0f)]
public readonly float sfxVolume;
[DxOptionalParameter(true)]
public readonly bool muteWhenUnfocused;
}
// Apply all defaults
audioSettings.Emit();
// Only change music volume
new AudioSettingsChanged(musicVolume: 0.5f).Emit();
// Change multiple settings
new AudioSettingsChanged(0.7f, 0.6f, 0.9f, false).Emit();
Best Practices¶
✅ Success: Recommendations
- Order matters — Place required fields before optional fields in your struct
- Use meaningful defaults — Choose defaults that represent the most common use case
- Prefer explicit values — Use
[DxOptionalParameter(0)]instead of[DxOptionalParameter]when clarity helps- Use Expression for complex types — Don't fight the type system; use
Expressionfor enums and Unity types- Document unusual defaults — If a default isn't obvious, add a comment explaining why
Why Use Attributes Instead of Manual Implementation¶
✅ Attribute Definition (Clean, Automatic)¶
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal
{
public readonly int amount;
}
What the generator emits (for reference)¶
// Auto-generated by DxMessaging (no need to hand-write this)
public readonly partial struct Heal : ITargetedMessage<Heal>
{
public readonly int amount;
public Heal(int amount)
{
this.amount = amount;
}
public Type MessageType => typeof(Heal);
}
Benefits¶
- ✅ Less code - 50% fewer lines
- ✅ Fewer bugs - Can't forget fields in constructor
- ✅ Focused - Focus on data, not boilerplate
- ✅ Refactor-safe - Add field? Constructor updates automatically!
Complete Example: Attribute Definition vs Generated Output¶
Attribute Definition (8 lines)¶
using DxMessaging.Core.Attributes;
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct PlayerDamaged
{
public readonly int amount;
public readonly string damageType;
public readonly GameObject source;
}
Generated Output (20 lines you never write)¶
// Auto-generated by DxMessaging (for reference only)
public readonly partial struct PlayerDamaged : IBroadcastMessage<PlayerDamaged>
{
public readonly int amount;
public readonly string damageType;
public readonly GameObject source;
public PlayerDamaged(int amount, string damageType, GameObject source)
{
this.amount = amount;
this.damageType = damageType;
this.source = source;
}
public Type MessageType => typeof(PlayerDamaged);
}
Result¶
- ✅ Same functionality
- ✅ Less code to maintain
- ✅ Automatically updates when you add/remove fields
- ✅ Works for class messages too
- ✅ Zero effort once you mark the struct partial
Advanced: Manual Implementation (When Attributes Aren't Enough)¶
Attributes cover almost every scenario. If you intentionally drop [DxTargetedMessage], [DxUntargetedMessage], or [DxBroadcastMessage], you'll need to hand-write the interface implementations and constructors shown in the “generated output” examples. Keep the attributes unless you have a very specific data-backed reason not to.
Generic Message Interfaces (Zero-Boxing for Structs)¶
readonly struct messages marked with the attributes already implement the generic interfaces, so emissions stay allocation-free. You get the same performance characteristics as the manual approach without writing any plumbing.
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal
{
public readonly int amount;
}
"Do I HAVE to use attributes?"¶
Technically no—but without them you must write the constructor, interface implementation, and MessageType property yourself (for speed, you can optionally leave this off, but it might box on certain call paths). Leaving the attributes on keeps everything consistent for the whole team.
"What if I want custom constructor logic?"¶
Keep the attributes and add a factory/helper so you still benefit from the generated constructor:
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct ComplexMessage
{
public readonly int value;
public static ComplexMessage FromRaw(int rawValue)
{
int clamped = Math.Clamp(rawValue, 0, 100);
return new ComplexMessage(clamped);
}
}
If you truly must write a custom constructor, drop [DxAutoConstructor] for that type but keep the [DxUntargetedMessage]/[DxTargetedMessage] attribute so the interface plumbing stays consistent.
"Can I mix attribute-based and manual messages?"¶
Yes. Attribute-driven messages happily coexist with string messages and any manual implementations you already have. You can migrate gradually by converting one message at a time:
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct MessageA
{
public readonly int value;
}
// Existing manual message types keep working alongside attribute-driven ones.
Extension Methods (Emit Helpers)¶
DxMessaging provides extension methods to make emitting messages easy:
using DxMessaging.Core.Extensions; // ← Don't forget this!
using UnityEngine;
// Untargeted (global)
var settings = new VideoSettingsChanged(1920, 1080);
settings.Emit();
// Targeted - GameObject overload
var heal = new Heal(10);
heal.EmitGameObjectTargeted(playerGameObject);
// Targeted - Component overload
heal.EmitComponentTargeted(playerComponent);
// Broadcast - GameObject overload
var dmg = new TookDamage(25);
dmg.EmitGameObjectBroadcast(enemyGameObject);
// Broadcast - Component overload
dmg.EmitComponentBroadcast(enemyComponent);
// String convenience (for quick prototyping)
"LevelCompleted".Emit();
Located in: DxMessaging.Core.Extensions.MessageExtensions
Automatic overload selection¶
- Extension methods pick the right overload based on type
- Defaults to global
MessageHandler.MessageBus - Pass custom bus with optional parameter
Local Bus Islands (Isolated Testing)¶
Create isolated message buses for tests or subsystems:
using DxMessaging.Core;
using DxMessaging.Core.MessageBus;
// Create isolated bus
var testBus = new MessageBus();
var handler = new MessageHandler(new InstanceId(1), testBus) { active = true };
var token = MessageRegistrationToken.Create(handler, testBus);
// Register on isolated bus
_ = token.RegisterUntargeted<MyMessage>(OnMessage);
// Emit to isolated bus (won't affect global bus!)
var msg = new MyMessage();
msg.Emit(testBus);
Use cases¶
- Unit tests (no global side effects!)
- Subsystem isolation (UI has own bus)
- Sandboxing (mod systems, untrusted code)
Attributes Quick Reference¶
Message Type Attributes¶
Untargeted¶
Mark as a global message (no target):
Targeted¶
Mark as a message sent to a specific target:
Broadcast¶
Mark as a message broadcast from a source:
Constructor Generation Attributes¶
[DxAutoConstructor]¶
Automatically generates a constructor with all public fields as parameters:
[DxAutoConstructor]
public readonly partial struct Damage
{
public readonly int amount;
public readonly string type;
}
// Generates: Damage(int amount, string type)
[DxOptionalParameter]¶
Makes a constructor parameter optional. Three usage patterns:
📋 Example: Default Value (type default)
Uses the type's default value (
0,false,null, etc.):📋 Example: Custom Value (primitive)
Provides a specific default value:
📋 Example: Expression (complex types)
Uses a verbatim expression for enums, Unity types, etc.:
Supported Types¶
These attributes work with:
- Top-level types — public structs and classes
- Nested types — types inside other classes
- Internal types — assembly-private messages
Common Patterns with Attributes¶
Pattern 1: Simple Message (No Data)¶
[DxUntargetedMessage]
public readonly partial struct GamePaused { }
// No constructor needed - empty message
Pattern 2: Message with Data¶
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct Heal
{
public readonly int amount;
}
// Auto-generates: Heal(int amount)
Pattern 3: Message with Optional Fields¶
[DxBroadcastMessage]
[DxAutoConstructor]
public readonly partial struct Attack
{
public readonly int damage;
[DxOptionalParameter]
public readonly string damageType;
}
// Auto-generates: Attack(int damage, string damageType = default)
Pattern 4: Complex Message¶
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct LevelCompleted
{
public readonly int level;
public readonly float time;
public readonly int score;
[DxOptionalParameter]
public readonly bool perfectRun;
[DxOptionalParameter]
public readonly string bonusReason;
}
// All fields become constructor parameters, last two are optional
Pattern 5: Nested Types¶
Define messages inside other classes for better organization:
public partial class GameEvents
{
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct LevelUp
{
public readonly int newLevel;
}
[DxTargetedMessage]
[DxAutoConstructor]
public readonly partial struct GainExperience
{
public readonly int amount;
}
}
// Usage:
var levelUp = new GameEvents.LevelUp(5);
levelUp.Emit();
Benefits¶
- Organizes related messages into namespaces or classes
- Reduces global namespace pollution
- Works identically to top-level messages
Pattern 6: Internal Types (Assembly-Private Messages)¶
Keep implementation details private to your assembly:
[DxUntargetedMessage]
[DxAutoConstructor]
internal readonly partial struct InternalDebugMessage
{
public readonly string debugInfo;
}
// Only visible within this assembly
// Perfect for internal messaging that shouldn't leak to other packages
Use cases¶
- Implementation details that shouldn't be public API
- Plugin or package-internal messaging
- Test-only messages
FAQ: Source Generators & Attributes¶
"Do I HAVE to use attributes?"¶
No! You can implement interfaces manually:
// Manual (works fine, just more code)
public readonly struct MyMsg : IUntargetedMessage<MyMsg>
{
public Type MessageType => typeof(MyMsg);
}
// Attributes (same result, less code)
[DxUntargetedMessage]
public readonly partial struct MyMsg { }
Recommendation: Use attributes unless you need explicit control.
"Why partial when using attributes?"¶
Source generators need to add code to your type. The partial keyword allows them to extend your struct/class in a separate file.
// Your file (MyMessage.cs)
[DxUntargetedMessage]
public readonly partial struct MyMessage { }
// Generated file (MyMessage.g.cs - auto-created!)
public readonly partial struct MyMessage : IUntargetedMessage<MyMessage>
{
public Type MessageType => typeof(MyMessage);
}
Forget partial? You'll get a compile error: "Type must be partial to use source generators"
"Can I see the generated code?"¶
Yes! In Visual Studio/Rider:
- Right-click on the message type
- Select "Go to Implementation" or "Go to Definition"
- You'll see the auto-generated file
Or check your obj/ folder for .g.cs files.
"What if I want custom constructor logic?"¶
Keep the attributes and wrap the generated constructor with a helper so you can inject custom logic without losing the source-generated plumbing:
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct ComplexMessage
{
public readonly int value;
public static ComplexMessage FromRaw(int rawValue)
{
int clamped = Math.Clamp(rawValue, 0, 100);
return new ComplexMessage(clamped);
}
}
If you truly need to hand-craft the constructor, drop [DxAutoConstructor] for that specific type but keep the [DxUntargetedMessage]/[DxTargetedMessage] attribute so the interface implementation is still generated.
"Do attributes affect runtime performance?"¶
No! Source generation happens at compile time. Generated code is identical to hand-written code. Zero runtime overhead.
"Can I mix attributes and manual implementation?"¶
Yes. Attribute-driven messages happily coexist with any legacy manual messages or string messages you already emit. Convert types gradually—one message at a time:
[DxUntargetedMessage]
[DxAutoConstructor]
public readonly partial struct MessageA
{
public readonly int value;
}
// Existing manual messages keep working alongside attribute-driven ones.
Troubleshooting Source Generators¶
⚠️ Warning: Attributes not working / code not generated
Checklist¶
- ✅ Is type marked
partial?- ✅ Did you rebuild the project?
- ✅ Is Unity 2021.3+ (Roslyn source generator support)?
- ✅ Check
obj/folder for.g.csfilesFix¶
C#// ❌ Missing partial, will not compile [DxAutoConstructor] public readonly struct MyMsg { } // ✅ Correct [DxAutoConstructor] public readonly partial struct MyMsg { }⚠️ Warning: Constructor not generated
Cause: No public fields to generate from
C#// ❌ No public fields [DxAutoConstructor] public readonly partial struct Empty { } // ✅ Has public field [DxAutoConstructor] public readonly partial struct WithData { public readonly int value; }⚠️ Warning: Unity can't find generated code
Solution¶
- Close Unity
- Delete
Library/folder- Reopen Unity
- Let it reimport everything
Related Documentation¶
- API Reference — Complete API documentation
- Message Types — When to use Untargeted/Targeted/Broadcast
- Quick Reference — Cheat sheet
- Design & Architecture — How source generation works internally
Summary¶
Source generators = Code wizards that write boilerplate for you¶
Use attributes for¶
- ✅ Clean, maintainable code
- ✅ Automatic constructor generation
- ✅ Zero boilerplate
- ✅ Refactor safety
Use manual implementation for¶
- ✅ Custom constructor logic
- ✅ Explicit control
- ✅ Understanding exactly what happens
Recommendation: Start with attributes (they cover most cases), switch to manual only when needed.