Category: RSCG

RSCG – rscg_demeter

RSCG – rscg_demeter    

name rscg_demeter
nuget https://www.nuget.org/packages/rscg_demeter/
link https://github.com/ignatandrei/rscg_demeter/
author Andrei Ignat

Generating diagnostics about Law of Demeter violations

 

This is how you can use rscg_demeter .

The code that you start with is

  <Project Sdk="Microsoft.NET.Sdk">  	<PropertyGroup> 		<OutputType>Exe</OutputType> 		<TargetFramework>net9.0</TargetFramework> 		<ImplicitUsings>enable</ImplicitUsings> 		<Nullable>enable</Nullable> 		<IsPackable>false</IsPackable> 		<TreatWarningsAsErrors>true</TreatWarningsAsErrors> 		<WarningsNotAsErrors>CS0436,NU1903</WarningsNotAsErrors>  	</PropertyGroup>  	<ItemGroup> 		<CompilerVisibleProperty Include="RSCG_Demeter_GenerateFile" /> 	</ItemGroup>  	<PropertyGroup> 		<RSCG_Demeter_GenerateFile>obj/gx/RSCG_Console.csproj.txt</RSCG_Demeter_GenerateFile> 	</PropertyGroup>   <ItemGroup>     <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.3" />     <PackageReference Include="RSCG_Demeter" Version="2026.328.706" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />   </ItemGroup>    	<PropertyGroup> 		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> 	</PropertyGroup> </Project>   

The code that you will use is

  // See https://aka.ms/new-console-template for more information using RSCG_Console;  Console.WriteLine("Hello, World!"); var dep = new Department(); dep.Employees.Add(new Employee());  foreach (var emp in dep.Employees) {     dep.EmployeeNames.Add(emp.Name); } var empAll = dep.Employees; var empWithA = empAll.Where(it => it.Name.StartsWith("a")); await Task.Run(dep.GetEmployees); var asda = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it)); Console.WriteLine(asda.Count); List<string> data = []; var d = AppDomain.CurrentDomain.GetAssemblies()     .Where(it => data.Any(a => !(it.FullName?.StartsWith(a) ?? false)))     .Distinct()     .ToArray();  var builder = new EmpBuilder().SetName("Ignat").SetId(1).SetName("Andrei"); var emp1 =builder.BuildEmployee();  

  The code that is generated is

 {   "dateGenerator": "20250329050839",   "nameGenerator": "Gerhart Hauptmann is feeling agreeable in Porto-Novo",   "maxDemeterDots": 3,   "locationsFound": 5,   "DemeterLocations": [     {       "id": 1,       "startLine": 5,       "nrDots": 2,       "endLine": 5,       "filePath": "D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\rscg_demeter\\src\\RSCG_Console\\Program.cs",       "text": "dep.Employees.Add(new Employee())"     },     {       "id": 2,       "startLine": 9,       "nrDots": 2,       "endLine": 9,       "filePath": "D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\rscg_demeter\\src\\RSCG_Console\\Program.cs",       "text": "    dep.EmployeeNames.Add(emp.Name)"     },     {       "id": 3,       "startLine": 12,       "nrDots": 2,       "endLine": 12,       "filePath": "D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\rscg_demeter\\src\\RSCG_Console\\Program.cs",       "text": "it.Name.StartsWith(\u0022a\u0022)"     },     {       "id": 4,       "startLine": 14,       "nrDots": 2,       "endLine": 14,       "filePath": "D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\rscg_demeter\\src\\RSCG_Console\\Program.cs",       "text": "empAll.Select(it =\u003E it.ID).Distinct().OrderBy(it =\u003E it)"     },     {       "id": 5,       "startLine": 17,       "nrDots": 3,       "endLine": 20,       "filePath": "D:\\gth\\RSCG_Examples\\v2\\rscg_examples\\rscg_demeter\\src\\RSCG_Console\\Program.cs",       "text": "AppDomain.CurrentDomain.GetAssemblies()\r\n    .Where(it =\u003E data.Any(a =\u003E !(it.FullName?.StartsWith(a) ?? false)))\r\n    .Distinct()\r\n    .ToArray()"     }   ] } 

Code and pdf at https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg_demeter

RSCG – PMart.Enumeration

RSCG – PMart.Enumeration    

name PMart.Enumeration
nuget https://www.nuget.org/packages/PMart.Enumeration.Generator/ https://www.nuget.org/packages/PMart.Enumeration/
link https://github.com/p-martinho/Enumeration
author Martinho

Constants as enumeration. With EFCore, Swagger and other implementations.

 

This is how you can use PMart.Enumeration .

The code that you start with is

  <Project Sdk="Microsoft.NET.Sdk">  	<PropertyGroup> 		<OutputType>Exe</OutputType> 		<TargetFramework>net9.0</TargetFramework> 		<ImplicitUsings>enable</ImplicitUsings> 		<Nullable>enable</Nullable> 	</PropertyGroup> 	<ItemGroup>  		<PackageReference Include="PMart.Enumeration" Version="3.1.0" /> 		<PackageReference Include="PMart.Enumeration.Generator" Version="3.1.0" PrivateAssets="all" ExcludeAssets="runtime" />  	</ItemGroup> 	<PropertyGroup> 		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> 	</PropertyGroup> </Project>   

The code that you will use is

  using DemoPMart;  var personType= PersonType.GetFromValueOrDefault("test"); Console.WriteLine(personType?.Value??"null"); personType = PersonType.GetFromValueOrDefault("manager"); Console.WriteLine(personType!.Value); Console.WriteLine(PersonType.Manager == personType);   
  using PMart.Enumeration.Generator.Attributes;  namespace DemoPMart; [Enumeration] public partial class PersonType {     private static readonly string ValueForEmployee = "Employee";     private static readonly string ValueForManager = "Manager";   }   

  The code that is generated is

 // <auto-generated> //     This code was generated by the PMart.Enumeration.Generator source generator. // </auto-generated>  #nullable enable  namespace DemoPMart {     public partial class PersonType : global::PMart.Enumeration.Enumeration<global::DemoPMart.PersonType>     {         [global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]         public static readonly global::DemoPMart.PersonType Employee = new global::DemoPMart.PersonType(ValueForEmployee!);          [global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]         public static readonly global::DemoPMart.PersonType Manager = new global::DemoPMart.PersonType(ValueForManager!);          [global::System.CodeDom.Compiler.GeneratedCodeAttribute("PMart.Enumeration.Generator", "3.1.0.0")]         private PersonType(string value) : base(value)         {         }     } } 

Code and pdf at https://ignatandrei.github.io/RSCG_Examples/v2/docs/PMart.Enumeration

RSCG – MemberAccessor

RSCG – MemberAccessor    

name MemberAccessor
nuget https://www.nuget.org/packages/BunnyTail.MemberAccessor/
link https://github.com/usausa/member-accessor-generator
author Yamaokuno

Generate getter and setter for class members.

 

This is how you can use MemberAccessor .

The code that you start with is

  <Project Sdk="Microsoft.NET.Sdk">    <PropertyGroup>     <OutputType>Exe</OutputType>     <TargetFramework>net9.0</TargetFramework>     <ImplicitUsings>enable</ImplicitUsings>     <Nullable>enable</Nullable>   </PropertyGroup> 	<ItemGroup> 		<PackageReference Include="BunnyTail.MemberAccessor" Version="1.2.0" /> 	</ItemGroup> 	<PropertyGroup> 		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> 	</PropertyGroup> </Project>   

The code that you will use is

  using BunnyTail.MemberAccessor; using DemoMember;  var accessorFactory = AccessorRegistry.FindFactory<Person>(); ArgumentNullException.ThrowIfNull(accessorFactory); var getter = accessorFactory.CreateGetter<string>(nameof(Person.FirstName)); var setter = accessorFactory.CreateSetter<string>(nameof(Person.FirstName)); ArgumentNullException.ThrowIfNull(getter); ArgumentNullException.ThrowIfNull(setter); var p= new Person(); setter(p, "andrei"); Console.WriteLine(getter(p));  
  using BunnyTail.MemberAccessor;  namespace DemoMember; [GenerateAccessor] internal class Person {     public string FirstName { get; set; }=string.Empty; }   

  The code that is generated is

 // <auto-generated /> #nullable enable  internal static class AccessorFactoryInitializer {     [global::System.Runtime.CompilerServices.ModuleInitializer]     public static void Initialize()     {         global::BunnyTail.MemberAccessor.AccessorRegistry.RegisterFactory(typeof(global::DemoMember.Person), typeof(global::DemoMember.Person_AccessorFactory));     } }  
 // <auto-generated /> #nullable enable  namespace DemoMember;  internal sealed class Person_AccessorFactory : global::BunnyTail.MemberAccessor.IAccessorFactory<global::DemoMember.Person> {     private static readonly global::System.Func<object, object?> ObjectFirstNameGetter = static x => ((global::DemoMember.Person)x).FirstName!;      private static readonly global::System.Action<object, object?> ObjectFirstNameSetter = static (x, v) => ((global::DemoMember.Person)x).FirstName = (string)v!;      private static readonly global::System.Func<global::DemoMember.Person, string> TypedFirstNameGetter = static x => x.FirstName;      private static readonly global::System.Action<global::DemoMember.Person, string> TypedFirstNameSetter = static (x, v) => x.FirstName = v;      public global::System.Func<object, object?>? CreateGetter(string name)     {         if (name == "FirstName") return ObjectFirstNameGetter;         return null;     }      public global::System.Action<object, object?>? CreateSetter(string name)     {         if (name == "FirstName") return ObjectFirstNameSetter;         return null;     }      public global::System.Func<global::DemoMember.Person, TProperty>? CreateGetter<TProperty>(string name)     {         if (name == "FirstName") return (global::System.Func<global::DemoMember.Person, TProperty>)(object)TypedFirstNameGetter;         return null;     }      public global::System.Action<global::DemoMember.Person, TProperty>? CreateSetter<TProperty>(string name)     {         if (name == "FirstName") return (global::System.Action<global::DemoMember.Person, TProperty>)(object)TypedFirstNameSetter;         return null;     } }  

Code and pdf at https://ignatandrei.github.io/RSCG_Examples/v2/docs/MemberAccessor

RSCG – StepwiseBuilderGenerator

RSCG – StepwiseBuilderGenerator    

name StepwiseBuilderGenerator
nuget https://www.nuget.org/packages/StepwiseBuilderGenerator/
link https://github.com/Georgiy-Petrov/StepwiseBuilderGenerator
author Georgiy Petrov

Generating Builder- as steps

 

This is how you can use StepwiseBuilderGenerator .

The code that you start with is

  <Project Sdk="Microsoft.NET.Sdk">    <PropertyGroup>     <OutputType>Exe</OutputType>     <TargetFramework>net8.0</TargetFramework> 	  <Nullable>enable</Nullable>   </PropertyGroup>  	  <PropertyGroup>         <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>         <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>     </PropertyGroup>  	  <ItemGroup> 	    <PackageReference Include="StepwiseBuilderGenerator" Version="1.0.3" /> 	  </ItemGroup>  	   	   </Project>   

The code that you will use is

  using Builder;  var pOld = new Person(); pOld.MiddleName = "G"; var pNew= pOld     .SetFirstNameBld("Andrei")     .SetLastNameBuilder("Ignat")     .Age(55)     .Build(it=>it)     ;    //var build = new PersonBuilder() //    .WithFirstName(pOld.FirstName) //    //.WithMiddleName("") // it is not into the constructor //    .WithLastName(pOld.LastName) //    ;      //var pNew = build.Build(); System.Console.WriteLine(pNew.FullName()); System.Console.WriteLine(pOld.FullName());   
  using StepwiseBuilderGenerator; using System;  namespace Builder; [StepwiseBuilder] public partial class Person {     public Person()     {         new GenerateStepwiseBuilder()            .AddStep<string>("SetFirstNameBld", "FirstName")            .AddStep<string>("SetLastNameBuilder", "LastName")            .AddStep<int>("Age")              .CreateBuilderFor<Person>();     }     //public Person(string firstName, string lastName)     //{     //    FirstName = firstName;     //    LastName = lastName;     //}     //public string FirstName { get; set; }     public string? MiddleName { get; set; }     //public string LastName { get; set; }      public string FullName()     {         return FirstName + " " + MiddleName + " "+LastName;     }      }   

  The code that is generated is

 using StepwiseBuilderGenerator; using System;  namespace Builder; public interface IPersonSetFirstNameBld  {     IPersonSetLastNameBuilder SetFirstNameBld(string value); } public interface IPersonSetLastNameBuilder  {     IPersonAge SetLastNameBuilder(string value); } public interface IPersonAge  {     IPersonBuild Age(int value); } public interface IPersonBuild  {     Person Build(Func<Person, Person> buildFunc); } public partial class Person : IPersonSetFirstNameBld,IPersonSetLastNameBuilder,IPersonAge,IPersonBuild  {    public string FirstName;     public string LastName;     public int AgeValue;      public IPersonSetLastNameBuilder SetFirstNameBld(string value)     {         FirstName = value;         return this;     }     public IPersonAge SetLastNameBuilder(string value)     {         LastName = value;         return this;     }     public IPersonBuild Age(int value)     {         AgeValue = value;         return this;     }     public Person Build(Func<Person, Person> buildFunc)     {         return buildFunc(this);     }      public enum Steps     {         SetFirstNameBld,         SetLastNameBuilder,         Age,     } } public static partial class StepwiseBuilders {     public static IPersonSetFirstNameBld Person()      {          return new Person();     } }  

Code and pdf at https://ignatandrei.github.io/RSCG_Examples/v2/docs/StepwiseBuilderGenerator

Demeter and Roslyn–part 3–config and tests


Beyond the Squiggles: Flexible Reporting and Confidence Through Testing

So, we’ve built our Law of Demeter analyzer, RSCG_Demeter, using Roslyn to intelligently spot those overly-chatty code chains, even navigating the complexities of fluent interfaces. We could just have it throw warnings or errors directly in your IDE via Roslyn diagnostics.

But let’s be honest, sometimes a flood of new diagnostics can feel a bit… harsh, especially when introducing a new rule to a large codebase. Maybe you want a summary report instead, something you can review offline or integrate into a CI process?

Flexibility First: Sending Demeter Violations to a File

I wanted to offer more control. What if you could just get a list of potential violations written to a file? Good news – you can!

RSCG_Demeter allows you to configure file output directly in your .csproj. Just add these settings:

<!-- In your .csproj file -->
<PropertyGroup>
  <!-- Make the build property visible to the analyzer -->
  <CompilerVisibleProperty Include="RSCG_Demeter_GenerateFile" />
</PropertyGroup>

<PropertyGroup>
  <!-- Define the output file path (relative to the csproj) -->
  <!-- Example: Output to 'DemeterReport.txt' in the project's parent directory -->
  <RSCG_Demeter_GenerateFile>../DemeterReport.txt</RSCG_Demeter_GenerateFile>
</PropertyGroup>

With this configuration, instead of diagnostics, the analyzer will neatly output all identified potential violations to the specified file.

How does it work under the hood? The analyzer uses Roslyn’s AnalyzerConfigOptionsProvider to read this custom MSBuild property. It even cleverly grabs the project directory (build_property.ProjectDir) to correctly resolve relative paths:

// Simplified snippet showing how the analyzer reads the setting
var writeToFile = context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
{
    // Try reading the custom build property
    provider.GlobalOptions.TryGetValue("build_property.RSCG_Demeter_GenerateFile", out var filePath);

    if (!string.IsNullOrWhiteSpace(filePath))
    {
        // Handle relative paths using the ProjectDir property
#pragma warning disable RS1035 // Analyzer correctness rule
        if (!Path.IsPathRooted(filePath) &amp;&amp;
            provider.GlobalOptions.TryGetValue("build_property.ProjectDir", out var csproj) &amp;&amp;
            !string.IsNullOrWhiteSpace(csproj))
        {
            filePath = Path.GetFullPath(Path.Combine(csproj, filePath));
        }
#pragma warning restore RS1035
        return filePath; // Return the absolute path
    }
    return string.Empty; // No file path configured
});

// Later, the analyzer checks if 'writeToFile' has a value
// and writes the report instead of creating diagnostics.

This gives you the flexibility to choose how you want to consume the Demeter analysis results – immediate feedback via diagnostics or a consolidated report via file output.

Building Confidence: It’s Tested!

An analyzer is only useful if it’s reliable. How do we know RSCG_Demeter correctly identifies violations and doesn’t flag valid patterns like our fluent builders? Testing!

The repository includes a dedicated test project (RSCGDemeter_Tests) packed with unit tests covering various scenarios, including:

  • Simple violations

  • Fluent interface patterns (return this)

  • Complex LINQ chains

  • Edge cases

These tests ensure the logic we discussed – from the initial dot counting to the semantic analysis of return types – works as expected.

Ready to Give it a Go?

Whether you prefer direct IDE feedback or a generated report, RSCG_Demeter aims to provide a robust and flexible way to encourage adherence to the Law of Demeter in your C# projects.

Check out the code, explore the tests, and try it on your solution!

➡️ https://github.com/ignatandrei/RSCG_Demeter

Let’s write less coupled, more maintainable C# together!


Demeter and Roslyn–part 2- code

Okay, let’s build on the first part and explain the “how” in a more engaging way, weaving in the code snippets to illustrate the process.


From Counting Dots to Understanding Code: Building the Demeter Analyzer

In the first part, we established our mission: build a Roslyn analyzer to sniff out Law of Demeter violations – stopping our code from “talking to strangers.” We also saw why the naive approach of just counting dots (.) runs into trouble with common patterns like fluent builders.

So, how do we get smarter about this?

Step 1: The Initial Scan (Finding Potential Candidates)

First, we need to find any method call chains that might be violations. We’re looking for code with multiple dots. Roslyn’s SyntaxProvider is perfect for this initial trawl. We tell it to look for InvocationExpressionSyntax (which represents a method call like DoSomething()) and then apply a quick filter:

// Using Roslyn's SyntaxProvider to find candidates

var lines = context.SyntaxProvider.CreateSyntaxProvider(
    // 1. Quick Check: Is it a method call?
    predicate: (sn, _) => sn is InvocationExpressionSyntax,
    // 2. Basic Filter: Does it have more than one dot?
    transform: (ctx, _) => FilterByDotCount(ctx)
)
.Where(it => it != null) // Filter out nodes that didn't pass
.Select((it,_) => it!); // Non-null assertion

// Helper to perform the simple dot count filter
private InvocationExpressionSyntax? FilterByDotCount(GeneratorSyntaxContext ctx)
{
    // We only care about InvocationExpressionSyntax
    if (!(ctx.Node is InvocationExpressionSyntax node)) return null;

    // Get the full text of the call chain (e.g., "a.b.c()")
    var text = node.ToFullString();
    if (string.IsNullOrEmpty(text)) return null;

    // Super basic check: count the dots.
    // If there's less than 2 dots (e.g., "a.b()" has 1), ignore it for now.
    var noDot = text.Replace(".", "");
    if (text.Length - noDot.Length < 2) return null; // Need at least two dots

    // It *might* be a violation, let's analyze further
    return node;
}

This gives us a list of syntax nodes that look suspicious based purely on the number of dots. But we know this includes false positives like builder.SetName(“X”).SetAge(30).

Step 2: The Semantic Deep Dive (Are We Talking to Ourselves?)

Now for the clever part. For each candidate identified above, we need to figure out if it’s a genuine violation or just a fluent interface talking to itself. This is where Roslyn’s Semantic Model comes in – it allows us to understand the meaning and types behind the syntax.

Here’s the core logic:

  1. We walk backward through the chain (from right to left).

  2. For each method call (InvocationExpressionSyntax) or property access (MemberAccessExpressionSyntax) in the chain, we use the semantic model to determine the return type.

  3. We keep track of the types we’ve seen in this specific chain.

  4. Crucially: If a method call returns the same type as a previous call in the same chain, we assume it’s a fluent pattern (like return this; or returning the same builder type). In this case, we don’t count the dot associated with that call as contributing to a Demeter violation.

// (Inside the main analysis loop for each candidate 'invocation')
List<ITypeSymbol> types = new(); // Track return types within this chain
bool isProblem = false;
int nrDots = 0; // Our adjusted dot count

// Get the starting point of the chain (e.g., 'a.b' in 'a.b.c()')
SyntaxNode? currentExpression = invocation.Expression;

// Walk the chain backwards (right-to-left)
while (currentExpression != null)
{
    ITypeSymbol? currentReturnType = null;

    // Is this part of the chain a method call? (e.g., the 'b()' in 'a.b().c()')
    if (currentExpression is InvocationExpressionSyntax inv)
    {
        // Use the Semantic Model to find the return type of this call
        currentReturnType = GetReturnTypeSymbol(semanticModel, inv); // Simplified helper call

        // Move to the next part of the chain (e.g., 'a' in 'a.b()')
        currentExpression = inv.Expression;
    }
    // Is this part of the chain a property/field access? (e.g., the '.b' in 'a.b.c()')
    else if (currentExpression is MemberAccessExpressionSyntax ma)
    {
        // We count this dot initially
        nrDots++;
        // Use the Semantic Model to find the type of the member being accessed
        currentReturnType = GetMemberTypeSymbol(semanticModel, ma); // Simplified helper call

        // Move to the next part of the chain (e.g., 'a' in 'a.b')
        currentExpression = ma.Expression;
    }
    else
    {
        // Reached the start or an unexpected syntax, stop walking
        break;
    }

    // --- The Fluent Interface Check ---
    if (currentReturnType != null)
    {
        // Have we seen this return type *before* in this specific chain?
        // We use SymbolEqualityComparer.Default for accurate type comparison.
        if (types.Contains(currentReturnType, SymbolEqualityComparer.Default))
        {
            // Yes! This looks like a fluent builder pattern.
            // Decrement the dot count we added earlier for this step.
            nrDots--;
        }
        else
        {
            // First time seeing this type in the chain, add it to our list.
            types.Add(currentReturnType);
        }
    }
} // End while loop walking the chain

// After checking the whole chain, is the adjusted dot count too high?
// (We generally consider > 1 adjusted dots a violation)
isProblem = nrDots > 1;

if (isProblem)
{
    // Report the violation! (Code to create diagnostic omitted for brevity)
    // ... uses invocation.GetLocation(), nrDots, etc.
}

By using the semantic model to track return types, we can intelligently discount the dots involved in fluent chains, giving us a much more accurate picture of potential Law of Demeter violations.

Try It Out!

This combination of initial syntax filtering and deeper semantic analysis allows RSCG_Demeter to flag overly chatty code while respecting common C# idioms.

Want to see it in action on your own codebase? Head over to the GitHub repository:

➡️ https://github.com/ignatandrei/RSCG_Demeter

Install the analyzer and see if your code is respecting the “Don’t talk to strangers” rule! Feedback and contributions are welcome!

Demeter and Roslyn–part 1–idea

Stop Your C# Code Talking to Strangers: Introducing a Law of Demeter Roslyn Analyzer

The Law of Demeter (LoD), often summarized as “Only talk to your immediate friends,” is a valuable principle for writing loosely coupled, maintainable code. Code that follows LoD tends to be less fragile and easier to refactor.

But manually checking for LoD violations? That’s tedious. Why not automate it?

That’s why I built RSCG_Demeter: a Roslyn Source Code Generator and Analyzer designed to detect potential Law of Demeter violations directly in your C# code.

Why It’s Tricker Than Just Counting Dots

While the simplest LoD violations look like obj.GetThing().GetAnotherThing().DoWork() (too many dots!), real-world C# throws curveballs:

  1. Fluent Interfaces (return this): Code like builder.SetName(“X”).SetAge(30) chains calls, but it’s calling methods on the same object. This is valid LoD, not talking to strangers! The analyzer needs to recognize this pattern.
    // Example: EmpBuilder returning 'this'
    public EmpBuilder SetName(string name) { /*...*/ return this; }
    // Usage: new EmpBuilder().SetName("Alice").SetRole("Dev");
    

     

     

  2. Complex LINQ Chains: Analyzing expressions like
     empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it)); 
    

    requires more than simple dot counting. We need to understand the sequence of operations and intermediate results.

  3. Extension Methods (like LINQ): How should we treat IEnumerable<T> extension methods? Technically, each call operates on the result of the previous one. RSCG_Demeter currently flags these as potential violations, promoting simpler data transformations, but this is a configurable aspect of LoD interpretation.
     empAll.Select(it => it.ID).Distinct().OrderBy(it => it)
    var ids = new List<int>(empAll.Select(it => it.ID).Distinct().OrderBy(it => it)); 
    

RSCG_Demeter in Action

This tool uses Roslyn’s powerful syntax and semantic analysis capabilities to look beyond simple dot counts and identify more nuanced potential LoD violations, while aiming to ignore common valid patterns like fluent builders.

Ready to enforce the Law of Demeter automatically?

Give RSCG_Demeter a try! Find the source code, usage instructions, and contribute here:
➡️ https://github.com/ignatandrei/RSCG_Demeter

Help your codebase avoid talking to strangers!

RSCG – EntityLengths.Generator

RSCG – EntityLengths.Generator
 
 

name EntityLengths.Generator
nuget https://www.nuget.org/packages/EntityLengths.Generator/
link https://github.com/TarasKovalenko/EntityLengths.Generator/
author Taras Kovalenko

Generating constants for max length for properties in entities

 

This is how you can use EntityLengths.Generator .

The code that you start with is


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.1" />
		<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
		</PackageReference>
		<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
		<PackageReference Include="EntityLengths.Generator" Version="1.0.3" />

	</ItemGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


Console.WriteLine("Hello, World!");
DbContextOptionsBuilder<DotNetStatsContext> optionsBuilder = new();
optionsBuilder.UseInMemoryDatabase("StatsDatabase");
var cnt = new DotNetStatsContext(optionsBuilder.Options);
await cnt.Database.EnsureCreatedAsync();
Console.WriteLine("Database created");
Console.WriteLine(cnt.Projects.Count());
Console.WriteLine("The max length of the Name property of the Project entity is: " + Constants.Project.NameLength);


global using Microsoft.EntityFrameworkCore;
global using Stats.Database;
using EntityLengths.Generator.Configuration;

[assembly: EntityLengthsGenerator(
    GenerateDocumentation = false,
    GeneratedClassName = "Constants",
    LengthSuffix = "Length",
    IncludeNamespaces = ["Stats.Database"],
    ExcludeNamespaces = [],
    ScanNestedNamespaces = true,
    ScanEntitySuffix = null,
    Namespace = "Stats.Database"
)]



namespace Stats.Database;
public partial class DotNetStatsContext : DbContext
{
    internal DotNetStatsContext() : base() { }
    public DotNetStatsContext(DbContextOptions<DotNetStatsContext> options)
        : base(options)
    {
        
    }
    
    public virtual DbSet<Project> Projects { get; set; }

    public virtual DbSet<Star> Stars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Project>(entity =>
        {
            entity.ToTable("Project");

            entity.Property(e => e.Id).HasColumnName("ID");
            entity.Property(e => e.Description).HasMaxLength(500);
            entity.Property(e => e.Name).HasMaxLength(50);
            entity.Property(e => e.SourceCodeUrl).HasMaxLength(50);
        });

        modelBuilder.Entity<Star>(entity =>
        {
            entity.Property(e => e.Id)
                .HasColumnName("ID");
            entity.Property(e => e.Idproject).HasColumnName("IDProject");
        });

        OnModelCreatingPartial(modelBuilder);
    }

    partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}


 

The code that is generated is

// <auto-generated/>
namespace Stats.Database;

public static partial class Constants 
{
	public static partial class Project
	{
		public const int DescriptionLength = 500;
		public const int NameLength = 50;
		public const int SourceCodeUrlLength = 50;
	}
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/EntityLengths.Generator

RSCG – RSCG_CompositeProvider

RSCG – RSCG_CompositeProvider
 
 

name RSCG_CompositeProvider
nuget https://www.nuget.org/packages/RSCG_CompositeProvider/
https://www.nuget.org/packages/RSCG_CompositeProvider_common/
link https://github.com/ignatandrei/RSCG_CompositeProvider
author Ignat Andrei

Generate composite class from interface, using multiple sources

 

This is how you can use RSCG_CompositeProvider .

The code that you start with is


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
	  <IsPackable>false</IsPackable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

  
  <ItemGroup>
    <PackageReference Include="RSCG_CompositeProvider" Version="2025.218.2100" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
    <PackageReference Include="RSCG_CompositeProvider_Common" Version="2025.218.2100" />
  </ItemGroup>
  <PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
</Project>


The code that you will use is


using CP_Console;

IDataValue provider = new DataValue_CP(new DataFromHttp(), new DataFromMemory());
var result = await provider.KeyFromValue("test", false);
Console.WriteLine(result);
DataValue_CP realClass = (DataValue_CP)provider ;
var lastInterface = realClass.lastUsedInterface ?? -1;
Console.WriteLine("value was obtained from " + realClass.Get(lastInterface).Name);


using RSCG_CompositeProvider_Common;

namespace CP_Console;
[CompositeProvider]
public interface IDataValue
{
    public string Name { get; set; }
    public Task<string> KeyFromValue(string key, bool defaultValue);

    
}




namespace CP_Console;

class DataFromHttp : IDataValue
{
    public string Name { get { return "DataFromHttp"; } set { } }

    public async Task<string> KeyFromValue(string key, bool defaultValue)
    {
        var http=new HttpClient();
        var result = await http.GetStringAsync("https://www."+ Guid.NewGuid().ToString()+".com/" + key);
        return result;
    }
}


class DataFromMemory : IDataValue
{
    public string Name { get { return "DataFromMemory"; } set { } }

    public async Task<string> KeyFromValue(string key, bool defaultValue)
    {
        await Task.Delay(1000);
        return $"this is value for {key} from memory";
    }
}

 

The code that is generated is


                // <auto-generated/>
                namespace Generated.CP_Console
                {
                    public static class TheAssemblyInfo
                    {
                        
                        public static readonly System.DateTime DateGeneratedUTC ;
                        public const string AssemblyName = "CP_Console";
                        public const string GeneratedNameNice = "Bertrand Russell is feeling good-natured in Rothera";
                        public const string GeneratedNameSmall = "good-natured-Bertrand Russell";
                        public const string GeneratedName = "good-natured-Bertrand Russell-Rothera";
                        static TheAssemblyInfo(){
                            DateGeneratedUTC = System.DateTime.ParseExact("2025-02-24 14:32:51", "yyyy-MM-dd HH:mm:ss", null);
                        }
                    }
                }
// <auto-generated>
    //     This code was generated by a tool :RSCG_CompositeProvider
    //     Runtime Version: Herta Müller is feeling amiable in George Town
    //     DateOfTool : 2025-02-18 17:23:31
    //     Changes to this file may cause incorrect behavior and will be lost if
    //     the code is regenerated.
    //</auto-generated>
//------------------------------------------------------------------------------
/// <summary>
    /// This static partial class is a composite provider of IDataValue objects. 
    ///</summary>

#nullable enable
#pragma warning disable CS8603
#pragma warning disable CS8625
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.CodeDom.Compiler.GeneratedCode("GeneratorName","2025.10218.11723.131")]
[System.Diagnostics.DebuggerDisplay("Name = {Name} ")]
public partial class DataValue_CP : global::CP_Console.IDataValue
{
public int? lastUsedInterface ;

private global::CP_Console.IDataValue[] values;
public DataValue_CP(params global::CP_Console.IDataValue[] values){
this.values=values;
}
public int Count{
get{
return values.Length;
}
}
public global::CP_Console.IDataValue Get(int nr){
    return values[nr];
}




        public string Name { get
        {
        lastUsedInterface = null;
        foreach(var item in values){
        lastUsedInterface =(lastUsedInterface ??-1)+1;
        if(item == null)continue;
        try{
        return item.Name;
        }
        catch(Exception ){
        //try with the next one
        }
        }
        throw new System.Collections.Generic.KeyNotFoundException();
        }
        set
        {
        foreach(var item in values){
        if(item == null)continue;
        try{
        item.Name = value;
        }
        catch(Exception ){
        //try with the next one
        }
        }
        }

        } 
    
            public virtual  async  System.Threading.Tasks.Task<string> KeyFromValue(string key, bool defaultValue) {
                lastUsedInterface =null;
                foreach(var item in values){
                    lastUsedInterface =(lastUsedInterface ??-1)+1;
                    if(item == null)continue;
                    try{
                        var data=   await  item.KeyFromValue(key, defaultValue) ;
                        return data;
                    }
                    catch(Exception ){
                        //try with the next one
                    }
                }
                throw new System.Collections.Generic.KeyNotFoundException();
            }
        
}

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/RSCG_CompositeProvider

RSCG – DependencyModules.SourceGenerator

RSCG – DependencyModules.SourceGenerator
 
 

name DependencyModules.SourceGenerator
nuget https://www.nuget.org/packages/DependencyModules.SourceGenerator/
https://www.nuget.org/packages/DependencyModules.Runtime/
link https://github.com/ipjohnson/DependencyModules
author Ian Johnson

Generating service dependencies from attributes.

Also,by the author, a more advanced example you will find in the DemoWithTest.zip inside the zip file

 

This is how you can use DependencyModules.SourceGenerator .

The code that you start with is


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
	<PropertyGroup>
		<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
		<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
	</PropertyGroup>
	<ItemGroup>
	  <PackageReference Include="DependencyModules.Runtime" Version="1.0.0-RC9074" />
	  <PackageReference Include="DependencyModules.SourceGenerator" Version="1.0.0-RC9074" />
	 <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />

	</ItemGroup>
</Project>


The code that you will use is


using DependencyModules.Runtime;
using InjectDemo;
using Microsoft.Extensions.DependencyInjection;


var serviceCollection = new ServiceCollection();

serviceCollection.AddModule<MyModule>();

var provider = serviceCollection.BuildServiceProvider();

var service = provider.GetService<Database>();

if(service == null)
    throw new Exception("Service not found");
else
    service.Open();


using DependencyModules.Runtime.Attributes;

[DependencyModule]
public partial class MyModule 
{ 

}


namespace InjectDemo
{
    internal interface IDatabase
    {
        public void Open();
    }
}


using DependencyModules.Runtime.Attributes;

namespace InjectDemo;
[SingletonService(ServiceType = typeof(Database))]
partial class Database : IDatabase
{
    private readonly IDatabase con;

    public Database(IDatabase con)
    {
        this.con = con;
    }
    public void Open()
    {
        Console.WriteLine($"open from database");
        con.Open();
    }

}





using DependencyModules.Runtime.Attributes;

namespace InjectDemo;
[SingletonService]
public partial class DatabaseCon:IDatabase
{
    public string? Connection { get; set; }
    public void Open()
    {
        Console.WriteLine("open from database con" );
    }
}



 

The code that is generated is

using DependencyModules.Runtime.Helpers;
using InjectDemo;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

public partial class MyModule
{
    private static int moduleField = DependencyRegistry<MyModule>.Add(ModuleDependencies);

    private static void ModuleDependencies(IServiceCollection services)
    {
        services.AddSingleton(typeof(Database), typeof(Database));
        services.AddSingleton(typeof(IDatabase), typeof(DatabaseCon));
    }
}

using BaseAttribute = System.Attribute;
using DependencyModules.Runtime.Helpers;
using DependencyModules.Runtime.Interfaces;
using Microsoft.Extensions.DependencyInjection;

#nullable enable
public partial class MyModule : IDependencyModule
{

    static MyModule()
    {
    }

    public void PopulateServiceCollection(IServiceCollection services)
    {
        DependencyRegistry<MyModule>.LoadModules(services, this);
    }

    void IDependencyModule.InternalApplyServices(IServiceCollection services)
    {
        DependencyRegistry<MyModule>.ApplyServices(services);
    }

    public override bool Equals(object? obj)
    {
        return obj is MyModule;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode());
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)]
    public partial class Attribute : BaseAttribute, IDependencyModuleProvider
    {

        public IDependencyModule GetModule()
        {
            var newModule = new MyModule();
            return newModule;
        }
    }
}
#nullable disable

Code and pdf at

https://ignatandrei.github.io/RSCG_Examples/v2/docs/DependencyModules.SourceGenerator

Andrei Ignat weekly software news(mostly .NET)

* indicates required

Please select all the ways you would like to hear from me:

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.