Avoid duplicating a type definition across assemblies

Rule ND2101 Source Files Organization

Warns about sharing code across assemblies via linked source files (> Add > Existing Items… > Add As Link), duplicating types at binary level: degraded maintainability and subtle versioning bugs. Each match is a duplicated type; unfold typesDefs to see the definitions.

Tolerable for some types shared across executables, e.g. startup concerns like assembly resolving handlers or .NET Framework version checks.

How to fix

Share types through DLLs, or rename same-full-name types that differ.

CQLinq source

This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.

// <Name>Avoid duplicating a type definition across assemblies</Name>
// <Id>ND2101:AvoidDuplicatingATypeDefinitionAcrossAssemblies</Id>

warnif count > 0

let groups = Application.Assemblies
             // Don't match types in ASP.NET Core generated assemblies 
             // whose name is suffixed with '.Views'
             .Where(a => !a.Name.EndsWith(".views", StringComparison.OrdinalIgnoreCase))
             .ChildTypes()
             .Where(t => !t.IsGeneratedByCompiler &&   
                          // Types created by the test infrastructure
                          t.FullName != "AutoGeneratedProgram" &&
                          // Allow to have multiple 'file' types in the same source file
                         !t.IsVisibleOnlyInFile &&
                          JustMyCode.Contains(t))
             .GroupBy(t => t.FullName)
from @group in groups
where @group.Count() > 1

// Tricky: This rule is executed on both current snapshot and baseline snapshot (if any).
//         Taking t as @group.First() could return any type in the group, and potentially
//         it can return different types for current and baseline snapshot.
//         As a result issues wouldn't corresponds (since types are different) and the user 
//         would see issues of this rule as a couple of issues added and removed.
//         Ordering types by parent assembly name and then taking the first type, discards this risk. 
let types = @group.OrderBy(t => t.ParentAssembly.Name).ToArray()

select new { 
   t = types[0],
   // In the 'types' column, make sure to group matched types by parent assemblies.
   typesDefs = types,
   assemblies = types.Select(t => t.ParentAssembly),
   tFullName = types[0].FullName,
   tCount = types.Length,
   Debt = (4 + 3*types.Length).ToMinutes().ToDebt(),
   Severity = Severity.High
}

//<Expl>
// There are {4} types with full name *{3}* declared in *{2}*.
// This leads to confusion and also naming collision.
//</Expl>

//<Desc>
// Warns about sharing code across assemblies via linked source files
// (*> Add > Existing Items… > Add As Link*), duplicating types at binary
// level: degraded maintainability and subtle versioning bugs. Each match is
// a duplicated type; unfold **typesDefs** to see the definitions.
//
// Tolerable for some types shared across executables, e.g. startup concerns
// like assembly resolving handlers or .NET Framework version checks.
//</Desc>

//<HowToFix>
// Share types through DLLs, or rename same-full-name types that differ.
//</HowToFix>

Source Files Organization: all 6 items