Rule Source Generator Deep Dive
Muonroi's source-generator workflow turns code-first rule authoring into a repeatable development loop:
- annotate methods with
[MExtractAsRule] - run the RuleGen CLI or let the source generators emit registration code
- compile with diagnostics that catch rule-graph mistakes early
- test execution with
MRuleOrchestratorSpy
This guide goes deeper than the quick RuleGen overview and focuses on what gets emitted, what diagnostics mean, and how to keep the workflow reliable in a real repo.
What the generator does
The source-generator layer serves two related but different goals:
- extraction tooling takes methods marked with
[MExtractAsRule]and helps you turn them into structured rule artifacts - the Roslyn source generator emits
RuleEngineRegistrationExtensions.g.csso discoveredIRule<TContext>implementations can be registered through DI automatically
The generated registration extension currently looks like:
namespace Muonroi.RuleEngine.Generated;
public static class RuleEngineRegistrationExtensions
{
public static IServiceCollection AddGeneratedRules(this IServiceCollection services)
{
// generated AddTransient<IRule<TContext>, TConcreteRule>() registrations
return services;
}
}
That gives you a simple developer loop:
- keep rule logic in normal C# files
- let generators and CLI tools handle the wiring and diagnostics
Attribute reference
The primary marker is:
[MExtractAsRule("RULE_CODE")]
The current attribute type is Muonroi.RuleEngine.Abstractions.MExtractAsRuleAttribute.
There is also a backward-compatible alias:
[ExtractAsRule("RULE_CODE")]
Required metadata
Code
Code is the constructor argument and must be unique in the logical rule set.
Good examples:
HIGH_VALUE_ORDERDEBT_RATIOPRICING_ENTERPRISE
Bad examples:
rule1- duplicated codes across the same workflow
- codes that encode environment or tenant names
Order
Order is optional in code, but if you use it you must understand its limitations. The analyzer already warns when you rely on Order without a dependency graph.
Example:
[MExtractAsRule("DEBT_RATIO", Order = 1)]
Optional metadata
HookPoint
HookPoint selects where the rule participates in the lifecycle. The attribute defaults to HookPoint.BeforeRule.
Example:
[MExtractAsRule("SCORE_ENRICHMENT", HookPoint = HookPoint.BeforeRule)]
DependsOn
DependsOn declares rule codes that must run first.
Example:
[MExtractAsRule("FINAL_APPROVAL", DependsOn = new[] { "CREDIT_SCORE", "DEBT_RATIO" })]
Prefer DependsOn over raw numeric ordering because the dependency graph is explicit and survives refactoring better.
Basic code-first example
using Muonroi.RuleEngine.Abstractions;
public sealed class DiscountRules
{
[MExtractAsRule("LOYALTY_DISCOUNT", Order = 0)]
public RuleResult Loyalty(OrderContext context, FactBag facts)
{
if (context.CustomerTier is "gold" or "platinum")
{
facts["discountPercent"] = 10;
return RuleResult.Success();
}
return RuleResult.Failure("Customer tier is not eligible.");
}
}
Pair that with the generated registration extension:
using Muonroi.RuleEngine.Generated;
builder.Services.AddRuleEngine<OrderContext>();
builder.Services.AddGeneratedRules();
What gets emitted
The current source generator scans classes that implement IRule<T> and emits DI registrations.
That means the generated code is about registration, not business logic transformation. Business logic extraction and file generation are handled by the CLI toolchain.
In practice the output gives you:
services.AddTransient<IRule<TContext>, ConcreteRule>()per discovered rule type- one generated extension method for the project
Benefits:
- fewer hand-maintained registration blocks
- fewer missing-DI bugs after refactors
- easier review because the wiring is deterministic
RuleGen CLI commands
The current CLI entry point supports:
extractverifyregistergenerate-testsmergesplitwatch
The CLI help text describes them as:
extract: extract methods marked with[MExtractAsRule]into rule classesverify: validate consistency, circular dependencies, and rule graph issuesregister: generate DI registration and optional dispatcher classesgenerate-tests: scaffold test files for extracted rulesmerge: merge runtime JSON, generated*.g.cs, or attributed source into a target classsplit: split rules from one file into individual rule fileswatch: auto-regenerate on source changes
Common command flows
Extract rules from source
muonroi-rule extract --source src/Handlers --output Generated/Rules
If you omit --output, the CLI infers a Rules folder relative to the source location.
Verify before commit
muonroi-rule verify --source-dir src/Handlers
Use verify in CI or as a pre-commit habit for rule-heavy projects.
Generate registration
muonroi-rule register \
--rules Generated/Rules \
--output Generated/MGeneratedRuleRegistrationExtensions.g.cs
Optional dispatcher output is also supported by the CLI.
Generate test scaffolds
muonroi-rule generate-tests --rules Generated/Rules --output tests/GeneratedRules
Merge generated rules into a partial class
muonroi-rule merge \
--rules-dir Generated/Rules \
--target src/Handlers/MyHandler.cs \
--namespace My.App \
--class MyHandler
The current merge command also supports source-folder based merging and multiple merge strategies.
Split attributed handlers back out
muonroi-rule split \
--source src/Handlers \
--output Generated/Rules \
--workflow loan-approval
Watch mode
muonroi-rule watch --source src/Handlers --output Generated/Rules
Watch mode is useful in developer workflows and editor integrations, but keep CI deterministic by running explicit one-shot commands there.
Configuration file
The CLI looks for .rulegenrc.json in the working directory.
Good uses for the config file:
- standard source directory
- standard output directory
- namespace override
- generated test settings
- team-wide command defaults
Use the config to keep scripts short and make local and CI behavior consistent.
Diagnostics overview
The current analyzer set goes beyond the original first five diagnostics. As of March 9, 2026, the package ships MRG001 through MRG009.
The five most important diagnostics for day-to-day work are still MRG001 to MRG005, so start there and then layer in the newer warnings.
MRG001: Duplicate Rule Code
Severity: Error
Meaning:
- the same rule code appears more than once in the analyzed set
Typical causes:
- copied rule method without changing the code
- merge conflict resolved incorrectly
- split/extract flow produced two rules with the same identifier
Fix:
- keep one canonical code
- rename the duplicate
- re-run
verify
MRG002: Invalid Hook Point
Severity: Error
Meaning:
- the value given for
HookPointis not valid for the enum
Typical causes:
- stale code after enum changes
- typo in manual attribute edits
Fix:
- use a valid
HookPointenum member - prefer compile-time enum references instead of strings in custom tooling
MRG003: Non-interface dependency
Severity: Warning
Meaning:
- a dependency field is a concrete type rather than an interface
Why it matters:
- rule classes become harder to test
- DI replacement becomes harder
- coupling grows silently
Fix:
- depend on interfaces where practical
- keep concrete dependencies rare and deliberate
MRG004: Helper method extraction failed
Severity: Warning
Meaning:
- RuleGen could not safely extract a helper method
Current expectation:
- private methods in the same class are supported best
Fix:
- keep helper methods private and local when you expect extraction support
- move shared logic into a separately testable service if the helper is too complex
MRG005: Missing DependsOn reference
Severity: Warning
Meaning:
- a rule declares a dependency on a code that does not exist
Typical causes:
- renamed upstream rule code
- typo in
DependsOn - partial refactor across multiple files
Fix:
- update the reference to the real rule code
- remove the dependency if it is no longer needed
Additional current diagnostics
The analyzer set currently also includes:
MRG006: order used withoutDependsOnMRG007: fact consumption without dependency pathMRG008: nullable assigned to non-nullable string riskMRG009: fact guard throwsInvalidOperationException
These warnings are especially useful in larger rule graphs where dependency order and fact production are no longer obvious from a single file.
How to think about Order vs DependsOn
Treat Order as a hint and DependsOn as the real execution relationship.
Why:
Orderis easy to abuse- numeric ordering hides intent
- dependency edges survive refactoring better
Good:
[MExtractAsRule("FINAL_APPROVAL", DependsOn = new[] { "CREDIT_SCORE", "DEBT_RATIO" })]
Weaker:
[MExtractAsRule("FINAL_APPROVAL", Order = 99)]
If you see MRG006, the analyzer is telling you the same thing.
Generated code review strategy
Generated files should be:
- deterministic
- short enough to diff comfortably
- excluded from manual business edits
Good review practice:
- review generated registration changes when rules are added or removed
- do not hand-edit generated outputs
- regenerate from source instead of patching generated files
VS Code extension
The repo also includes a VS Code extension with:
- rule extraction commands
- CodeLens integration
- diagnostics integration
- watch mode helpers
Useful commands exposed by the extension include:
- extract all rules
- watch mode
- go to rule
Cross-reference RuleGen VS Code Extension for the editor-facing workflow.
Testing with MRuleOrchestratorSpy
MRuleOrchestratorSpy<TContext> captures:
- execution records
- the final fact snapshot
- per-rule success and duration
- fact changes for fired rules
Example:
var rules = new IRule<OrderContext>[]
{
new LoyaltyDiscountRule()
};
var spy = new MRuleOrchestratorSpy<OrderContext>(rules);
FactBag facts = await spy.ExecuteAsync(new OrderContext("gold"));
Assert.Single(spy.ExecutionRecords);
Assert.True(spy.ExecutionRecords[0].IsSuccess);
Assert.Equal(10, facts["discountPercent"]);
Use the spy when you want integration-style confidence without spinning up the whole runtime host.
netstandard2.0 and source-generator constraints
The repo still carries some compatibility constraints that matter for generator and CLI code:
- use
"\n"instead ofEnvironment.NewLinein generated output - avoid
ToHashSet()in compatibility-sensitive code paths - use the compatible
string.Replaceoverloads expected by the target - keep the
IsExternalInitpolyfill when record-like patterns need it
These constraints are easy to forget if you mostly work in newer .NET targets, so keep them in mind when changing generator code.
Common mistakes
Treating generated code as source of truth
Generated code is a build artifact. The source method and attribute metadata remain the source of truth.
Encoding hidden workflow meaning in rule codes
Keep rule codes stable, short, and business-relevant. Do not encode environments, machines, or tenant ids in the code unless that is truly the business identity of the rule.
Depending on concrete services everywhere
This makes MRG003 inevitable and makes rule testing harder.
Using exceptions for normal missing-fact logic
MRG009 exists because exception-driven rule flow becomes noisy and expensive. Prefer explicit failure results when a missing fact is part of expected control flow.
Skipping verify
Extraction can succeed while the dependency graph is still wrong. verify is what catches those issues before runtime.
Suggested CI workflow
For rule-heavy repositories, a reliable CI sequence is:
- restore tools and packages
- run
muonroi-rule verify --source-dir ... - build the solution so Roslyn diagnostics fail fast
- run unit tests or spy-based integration tests
This keeps rule graph problems out of runtime environments.
FAQ
Do I need both the CLI and the source generator?
Usually yes.
- the source generator helps with compile-time DI registration
- the CLI helps with extraction, verification, merge, split, and watch workflows
Should I commit generated files?
Commit policy is a repo decision. If your team commits them, treat them as derived artifacts and regenerate rather than hand-editing.
Can I use the workflow without the VS Code extension?
Yes. The CLI and generators are independent of the editor integration.
What is the minimum habit that prevents most problems?
Keep rule codes unique, use DependsOn, and run verify before merging.