@@ -22,12 +22,6 @@ public List<ExtractedEntity> AnalyzeEntities(string sourceDirectory, ExtractionO
2222 var entities = new List < ExtractedEntity > ( ) ;
2323 var csFiles = FindFiles ( sourceDirectory , options . BusinessObjectPatterns , options . ExcludePatterns ) . ToList ( ) ;
2424
25- // Resolve ORM type
26- var ormType = options . Orm == OrmType . Auto
27- ? DetectOrmType ( csFiles )
28- : options . Orm ;
29- options . ResolvedOrm = ormType ;
30-
3125 // Parsed once and kept, because the DbSet roster has to be known before the first class is
3226 // classified and re-parsing every file to build it costs more than holding the trees.
3327 //
@@ -44,6 +38,13 @@ public List<ExtractedEntity> AnalyzeEntities(string sourceDirectory, ExtractionO
4438
4539 var roster = DbSetRoster . Read ( parsedFiles . Select ( parsed => parsed . Root ) ) ;
4640
41+ // Resolved after the parse, because the roster is the best evidence there is and it only
42+ // exists once the trees do.
43+ var ormType = options . Orm == OrmType . Auto
44+ ? DetectOrmType ( parsedFiles . Select ( parsed => parsed . Root ) , roster )
45+ : options . Orm ;
46+ options . ResolvedOrm = ormType ;
47+
4748 foreach ( var ( file , root ) in parsedFiles )
4849 {
4950 var classDeclarations = root . DescendantNodes ( ) . OfType < ClassDeclarationSyntax > ( ) ;
@@ -475,17 +476,73 @@ private static List<ExtractedAppearanceRule> ExtractAppearanceRules(ClassDeclara
475476 /// <summary>
476477 /// Detects ORM mode by scanning file contents for EF-specific namespaces.
477478 /// </summary>
478- private static OrmType DetectOrmType ( IEnumerable < string > csFiles )
479+ /// <summary>
480+ /// Decides which ORM an application persists with, from what its source declares.
481+ /// </summary>
482+ /// <remarks>
483+ /// Read as syntax rather than as text. Scanning file contents for a namespace counts a mention
484+ /// of it in a comment, a string or an <c>#if</c>-disabled block as evidence — and a project
485+ /// that merely discusses EF Core is not one that uses it.
486+ /// <para>
487+ /// The signals are ranked by what each one costs to be wrong about. A <c>DbSet<T></c>
488+ /// registered on a context is the application declaring a table, and it cannot be mistaken;
489+ /// a <c>using</c> directive is weaker but still deliberate. Where neither ORM leaves any
490+ /// trace the answer is <see cref="OrmType.Unknown"/>, because the alternative is to state a
491+ /// default in the same voice as everything that was actually read.
492+ /// </para>
493+ /// </remarks>
494+ private static OrmType DetectOrmType ( IEnumerable < SyntaxNode > roots , DbSetRoster roster )
479495 {
480- foreach ( var file in csFiles )
496+ // The application cannot run without its registrations being right, which makes them the
497+ // one signal that is never incidental.
498+ if ( roster . RegistersAnything )
499+ return OrmType . EfCore ;
500+
501+ var efCore = false ;
502+ var xpo = false ;
503+
504+ foreach ( var root in roots )
481505 {
482- var source = File . ReadAllText ( file ) ;
483- if ( source . Contains ( "DevExpress.Persistent.BaseImpl.EF" ) )
484- return OrmType . EfCore ;
506+ foreach ( var directive in root . DescendantNodes ( ) . OfType < UsingDirectiveSyntax > ( ) )
507+ {
508+ if ( directive . Alias is not null || directive . Name is null )
509+ continue ;
510+
511+ var name = directive . Name . ToString ( ) ;
512+
513+ // Exact or namespace-prefixed, never StartsWith on its own:
514+ // `DevExpress.Persistent.BaseImpl` is XPO and a prefix of the EF Core one.
515+ if ( IsOrDescends ( name , "Microsoft.EntityFrameworkCore" )
516+ || IsOrDescends ( name , "DevExpress.Persistent.BaseImpl.EF" )
517+ || IsOrDescends ( name , "DevExpress.ExpressApp.EFCore" ) )
518+ efCore = true ;
519+ else if ( IsOrDescends ( name , "DevExpress.Xpo" ) )
520+ xpo = true ;
521+ }
522+
523+ // A base class is as deliberate as a using directive and survives file-scoped
524+ // namespaces that name nothing.
525+ foreach ( var classDecl in root . DescendantNodes ( ) . OfType < ClassDeclarationSyntax > ( ) )
526+ {
527+ foreach ( var baseTypeName in GetBaseTypeNames ( classDecl ) )
528+ {
529+ if ( baseTypeName is "XPObject" or "XPCustomObject" or "XPLiteObject" or "XPBaseObject" )
530+ xpo = true ;
531+ else if ( baseTypeName is "DbContext" )
532+ efCore = true ;
533+ }
534+ }
485535 }
486- return OrmType . Xpo ;
536+
537+ if ( efCore ) return OrmType . EfCore ;
538+ if ( xpo ) return OrmType . Xpo ;
539+
540+ return OrmType . Unknown ;
487541 }
488542
543+ private static bool IsOrDescends ( string name , string ns ) =>
544+ name . Equals ( ns , StringComparison . Ordinal ) || name . StartsWith ( $ "{ ns } .", StringComparison . Ordinal ) ;
545+
489546 /// <summary>
490547 /// The types an application registers as <c>DbSet<T></c> on a <c>DbContext</c>, and the
491548 /// namespaces each registration could have been naming.
@@ -516,6 +573,9 @@ private sealed class DbSetRoster
516573 {
517574 private readonly List < ( string Argument , HashSet < string > Scopes ) > _registrations = [ ] ;
518575
576+ /// <summary>Whether any context in the source registers anything at all.</summary>
577+ public bool RegistersAnything => _registrations . Count > 0 ;
578+
519579 /// <summary>
520580 /// Reads every <c>DbSet<T></c> property declared on a context in the parsed source.
521581 /// </summary>
0 commit comments