Wie funktioniert Entity Framework mit rekursiven Hierarchien? Include () scheint damit nicht zu funktionieren

72

Ich habe eine Item. Itemhat eine Category.

Categoryhat ID, Name, Parentund Children. Parentund Childrensind auch von Category.

Wenn ich eine LINQ to Entities-Abfrage für eine bestimmte Methode durchführe, wird Itemdie zugehörige nicht zurückgegeben Category, es sei denn, ich verwende die Include("Category")Methode. Aber es bringt nicht die vollständige Kategorie mit seinen Eltern und Kindern. Ich könnte es tun Include("Category.Parent"), aber dieses Objekt ist so etwas wie ein Baum, ich habe eine rekursive Hierarchie und ich weiß nicht, wo es endet.

Wie kann ich EF dazu bringen Category, das mit Eltern und Kindern und die Eltern mit ihren Eltern und Kindern usw. vollständig zu laden ?

Dies gilt nicht für die gesamte Anwendung. Aus Leistungsgründen wäre dies nur für diese bestimmte Entität, die Kategorie, erforderlich.

Victor Rodrigues
quelle
2
Wenn es rekursiv ist, könnte es (leicht) einen Zyklus haben. Sie müssen eine maximale Tiefe wählen. Anschließend können Sie eine Abfrage dafür schreiben.
Craig Stuntz
2
Nein, Zyklen sind nicht erlaubt. Die Geschäftsschichten sehen dies als Baum, keine Chance für die Eltern, ein Kind als Eltern hinzuzufügen.
Victor Rodrigues
Wenn Zyklen nicht zulässig sind, sind verschachtelte Mengen möglicherweise das beste relationale Modell. Hängt jedoch von Ihrer App ab. NS eignet sich hauptsächlich zum Abfragen, FKs eignen sich zum Einfügen / Aktualisieren / Löschen. Die Zuordnung wäre jedoch völlig anders.
Craig Stuntz
2
Ich weiß nicht, dass Sie das Modell ändern müssen. Verschachtelte Mengen sind jedoch bei der primären Auswahl erheblich schneller. RDBMS haben normalerweise entweder (1) überhaupt keine Unterstützung für Erben (z. B. SQL 2005) oder (2) haben sie bis zu einem gewissen Grad, stellen sie jedoch nicht in ihrem EF-Anbieter zur Verfügung (z. B. SQL 2008). Sie bitten die EF, etwas zu unterstützen, was die DB in reinem SQL nicht kann! (Überlegen Sie: Welche SQL-Anweisung würde JOINed-Zeilen mit unbegrenzter Tiefe zurückgeben?) Das eifrige Laden in EF funktioniert durch Ändern der SQL-Anweisung. Welche SQL-Anweisung würde die gewünschte Ergebnismenge erzeugen?
Craig Stuntz
2
Nur weil Ihre Implementierung keine Zyklen zulässt, können die von Ihnen verwendeten Datenkonstrukte. Daher kann EF nicht zulassen, dass Sie den Baum eifrig laden. Stattdessen müssen Sie Load explizit aufrufen, wenn Sie die zugehörigen Elemente benötigen. Alternativ können Sie die Tabelle als flache Sammlung laden und den Baum manuell neu erstellen. Dies würde zwar eine einzelne Anforderung zum Lesen an die Datenbank senden, die Verwaltung von Updates wird jedoch schwieriger.
Jim Wooley

Antworten:

24

Anstatt die IncludeMethode zu verwenden, die Sie verwenden könnten Load.

Sie könnten dann für jedes eine machen und alle Kinder durchlaufen und ihre Kinder laden. Dann machen Sie eine für jeden durch ihre Kinder und so weiter.

Die Anzahl der Ebenen, die Sie nach unten gehen, wird in der Anzahl für jede Schleife, die Sie haben, fest codiert.

Hier ist ein Beispiel für die Verwendung von Load: http://msdn.microsoft.com/en-us/library/bb896249.aspx

Shiraz Bhaiji
quelle
Ich habe Load ausprobiert, aber eine SqlDataReader-Ausnahme wurde ausgelöst (auch wenn die MultipleActiveResultSets aktiviert wurden)
Victor Rodrigues
Das Problem war SQL Server 2000. MARS wird nicht unterstützt.
Victor Rodrigues
36
Dies ist aus Leistungsgründen eine wirklich schlechte Lösung: Jede Ebene ist eine andere Reise in die Datenbank, und es wird noch schlimmer, wenn Sie das verzögerte Laden aktiviert haben. mehr hier: stackoverflow.com/a/22024714/237723
JoeBrockhaus
@reggaeguitar Diese Frage wurde mit nicht so tollen Antworten überflutet. Das Kopieren und Einfügen des gleichen Codes aus der gründlicheren Antwort auf eine andere Frage wäre verschwenderisch. Auch 18> 5 stackoverflow.com/questions/2266473/…
JoeBrockhaus
1
@ JoeBrockhaus Ich bezog mich auf den MSDN-Link
Reggaeguitar
14

Wenn Sie definitiv möchten, dass die gesamte Hierarchie geladen wird, würde ich versuchen, eine gespeicherte Prozedur zu schreiben, deren Aufgabe es ist, alle Elemente in einer Hierarchie zurückzugeben und das Element zurückzugeben, nach dem Sie zuerst fragen (und anschließend die untergeordneten Elemente).

Lassen Sie dann die Beziehungskorrektur der EF sicherstellen, dass alle miteinander verbunden sind.

dh so etwas wie:

// the GetCategoryAndHierarchyById method is an enum
Category c = ctx.GetCategoryAndHierarchyById(1).ToList().First();

Wenn Sie Ihre gespeicherte Prozedur korrekt geschrieben haben, sollte durch das Materialisieren aller Elemente in der Hierarchie (dh ToList()) die EF-Beziehungskorrektur aktiviert werden.

Und dann sollten für das gewünschte Element (First ()) alle untergeordneten Elemente geladen sein und die untergeordneten Elemente usw. geladen sein. Alle Elemente werden von diesem einen Aufruf einer gespeicherten Prozedur ausgefüllt, sodass auch keine MARS-Probleme auftreten.

Hoffe das hilft

Alex

Alex James
quelle
1
Dies funktioniert nur, wenn Ihr SPROC der Kategorieentität in Ihrem Eigenschaftenfenster für den Funktionsimport zugeordnet ist und nur diese Daten zurückgibt. Dh: Wenn Ihr SPROC eine Verknüpfung mit EntityTableAund EntityTableB& Ihr Ziel ist, eine Ergebnissammlung von EntityTableAEntitäten mit Navigationseigenschaften für EntityTableBEntitäten zu haben, können Sie diese Methode NICHT verwenden. Stattdessen müssen Sie Ihren abgeflachten Datensatz entsprechend gruppieren und in die entsprechenden Entitäten konvertieren.
JoeBrockhaus
1
Wenn Sie SQL Server verwenden und sich für die SP-Lösung entscheiden, können Sie CTE verwenden, um die Hierarchie in einer Abfrage abzurufen. Siehe: technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
Assaf S.
6

Es könnte gefährlich sein, wenn Sie alle rekursiven Entitäten laden, insbesondere in Bezug auf die Kategorie. Sie könnten am Ende viel mehr erhalten, als Sie erwartet hatten:

Category > Item > OrderLine > Item
                  OrderHeader > OrderLine > Item
         > Item > ...

Plötzlich haben Sie den größten Teil Ihrer Datenbank geladen. Sie könnten auch Rechnungszeilen, dann Kunden und dann alle anderen Rechnungen geladen haben.

Was Sie tun sollten, ist etwa Folgendes:

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select q;

foreach (Category cat in qryCategories) {
    if (!cat.Items.IsLoaded)
        cat.Items.Load();
    // This will only load product groups "once" if need be.
    if (!cat.ProductGroupReference.IsLoaded)
        cat.ProductGroupReference.Load();
    foreach (Item item in cat.Items) {
        // product group and items are guaranteed
        // to be loaded if you use them here.
    }
}

Eine bessere Lösung besteht jedoch darin, Ihre Abfrage so zu erstellen, dass eine anonyme Klasse mit den Ergebnissen erstellt wird, sodass Sie Ihren Datenspeicher nur einmal aufrufen müssen.

var qryCategories = from q in ctx.Categories
                    where q.Status == "Open"
                    select new {
                        Category = q,
                        ProductGroup = q.ProductGroup,
                        Items = q.Items
                    };

Auf diese Weise können Sie bei Bedarf ein Wörterbuchergebnis zurückgeben.

Denken Sie daran, dass Ihre Kontexte so kurzlebig wie möglich sein sollten.

Brett Ryan
quelle
1
Dies führt zu vielen Roundtrips zur Datenbank, die durch eifriges Laden vermieden werden sollen.
Asad Saeeduddin
2
Genau aus diesem Grund habe ich gesagt: "Eine bessere Lösung besteht jedoch darin, Ihre Abfrage so zu erstellen, dass eine anonyme Klasse mit den Ergebnissen erstellt wird, sodass Sie Ihren Datenspeicher nur einmal aufrufen müssen." Beachten Sie auch, dass Ihre Referenzdaten nur einmal geladen werden, was zu weniger Treffern während der Kontextlebensdauer führt.
Brett Ryan
1
Ah ich sehe; Entschuldigung für die Ablehnung. Ich habe gerade den Teil "Das sollten Sie tun" gesehen, gefolgt von dem, was das OP meiner Meinung nach nicht tun sollte. Wenn Sie die Antwort bearbeiten (vielleicht "Dies ist, was Sie tun sollten" qualifizieren?), Kann ich meine Stimme umkehren.
Asad Saeeduddin
5

Sie möchten die Hierarchie nicht rekursiv laden, es sei denn, Sie erlauben einem Benutzer, den Baum iterativ auf- oder abzusuchen: Jede Rekursionsstufe ist eine weitere Reise in die Datenbank. In ähnlicher Weise möchten Sie ein verzögertes Laden, um weitere DB-Auslösungen zu verhindern, wenn Sie beim Rendern auf eine Seite oder beim Senden über einen Webservice die Hierarchie durchlaufen.

Drehen Sie stattdessen Ihre Abfrage um: Get Catalogund Includedie darin enthaltenen Elemente. Auf diese Weise erhalten Sie alle Elemente sowohl hierarchisch (Navigationseigenschaften) als auch abgeflacht. Jetzt müssen Sie nur noch die am Stamm vorhandenen Nicht-Root-Elemente ausschließen, was ziemlich trivial sein sollte.

Ich hatte dieses Problem und lieferte ein detailliertes Beispiel dieser Lösung zu einem anderen, hier

JoeBrockhaus
quelle
5

Verwenden Sie diese Erweiterungsmethode, die die fest codierte Version von aufruft. IncludeUm eine dynamische Einschlussstufe zu erreichen, funktioniert sie hervorragend.

namespace System.Data.Entity
{
  using Linq;
  using Linq.Expressions;
  using Text;

  public static class QueryableExtensions
  {
    public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
      int levelIndex, Expression<Func<TEntity, TEntity>> expression)
    {
      if (levelIndex < 0)
        throw new ArgumentOutOfRangeException(nameof(levelIndex));
      var member = (MemberExpression)expression.Body;
      var property = member.Member.Name;
      var sb = new StringBuilder();
      for (int i = 0; i < levelIndex; i++)
      {
        if (i > 0)
          sb.Append(Type.Delimiter);
        sb.Append(property);
      }
      return source.Include(sb.ToString());
    }
  }
}

Verwendung:

var affiliate = await DbContext.Affiliates
  .Include(3, a => a.Referrer)
  .SingleOrDefaultAsync(a => a.Id == affiliateId);

Nehmen Sie in der Zwischenzeit an der Diskussion über das EF-Repo teil.

Shimmy Weitzhandler
quelle
Das ist toll. Und die Diskussion ist großartig. Ich bin mir jedoch nicht sicher, wie ich Ihren Ausdruck in diesem Fall verwenden soll. Selbst in Ihrem Beispiel funktioniert das nicht, wenn Sie Include (4, m => m.Referred) verwenden? Wenn Ihr Queryable IQueryable <Affiliate> ist, ist Ihr Func-Rückgabewert ein Array?
Jsgoupil
@jsgoupil, nein. Wie Sie sehen können, ist der Rückgabewert vom genauen Typ als Eingabeparameter IQueryable<TEntity>und gilt für die verzögerte Ausführung. Sie können dies als fließende API-Abfrage und eine der IncldeErweiterungsmethoden mehrmals für eine einzelne Abfrage verwenden, um verschiedene Zweige in die Rückgabedaten aufzunehmen.
Shimmy Weitzhandler
Es tut mir leid, ich bin ein bisschen ratlos. Können Sie ein Beispiel dafür geben, wie Sie es in Kürze mit dem Beispiel "Klasse Partner" verwenden können?
Jsgoupil
1
@jsgoupil Ich habe meine Antwort gemäß Ihrer Anfrage aktualisiert. Sollte etwas unklar sein, sprich mit mir. Das Argument 3für den levelIndexParameter gibt die Tiefenebene an, von der die untergeordneten Elemente geladen werden sollen.
Shimmy Weitzhandler
Danke, das macht Sinn. Ich war früher verwirrt, weil das Beispiel in anderen Antworten von "Kindern" und nicht von "Kind" wie in Ihrem Beispiel sprach. Vielen Dank für die Verwendung hier.
Jsgoupil
3

Sie sollten lieber eine Zuordnungstabelle einführen, die jeder Kategorie ein übergeordnetes und ein untergeordnetes Element zuordnet, anstatt die übergeordnete und untergeordnete Eigenschaft der Ladung selbst hinzuzufügen.

Je nachdem, wie oft Sie diese Informationen benötigen, können sie bei Bedarf abgefragt werden. Durch eindeutige Einschränkungen in der Datenbank können Sie vermeiden, dass unendlich viele Beziehungen möglich sind.

Johannes Rudolph
quelle
2

Und nun zu einem völlig anderen Ansatz für hierarchische Daten, zum Beispiel zum Auffüllen einer Baumansicht.

Führen Sie zunächst eine flache Abfrage für alle Daten durch und erstellen Sie dann das Objektdiagramm im Speicher:

  var items = this.DbContext.Items.Where(i=> i.EntityStatusId == entityStatusId).Select(a=> new ItemInfo() { 
            Id = a.Id,
            ParentId = a.ParentId,
            Name = a.Name,
            ItemTypeId = a.ItemTypeId
            }).ToList();

Holen Sie sich das Root-Element:

 parent = items.FirstOrDefault(a => a.ItemTypeId == (int)Enums.ItemTypes.Root);

Erstellen Sie nun Ihr Diagramm:

 this.GetDecendantsFromList(parent, items);


 private void GetDecendantsFromList(ItemInfo parent, List<ItemInfo> items)
    {
        parent.Children = items.Where(a => a.ParentId == parent.Id).ToList();
        foreach (var child in parent.Children)
        {
            this.GetDecendantsFromList(child,items);
        }
    }
Greg Gum
quelle
1

Hier ist eine clevere rekursive Funktion, die ich hier gefunden habe und die dafür funktionieren würde:

public partial class Category
{
    public IEnumerable<Category> AllSubcategories()
    {
        yield return this;
        foreach (var directSubcategory in Subcategories)
            foreach (var subcategory in directSubcategory.AllSubcategories())
            {
                yield return subcategory;
            }
    }
}
Parlament
quelle
1
Dies ist ideal für eine In-Memory-Sammlung, aber ich würde dies nicht für das aktive Abrufen von Ergebnissen aus einer Datenbank empfehlen. n + 1 FTL stackoverflow.com/questions/97197/what-is-the-n1-selects-issue
JoeBrockhaus
1

Sie können auch eine Tabellenwertfunktion in der Datenbank erstellen und diese Ihrem DBContext hinzufügen. Dann können Sie das von Ihrem Code aus aufrufen.

In diesem Beispiel müssen Sie EntityFramework.Functions aus Nuget importieren.

public class FunctionReturnType
{
    public Guid Id { get; set; } 

    public Guid AnchorId { get; set; } //the zeroPoint for the recursion

    // Add other fields as you want (add them to your tablevalued function also). 
    // I noticed that nextParentId and depth are useful
}

public class _YourDatabaseContextName_ : DbContext
{
    [TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
    public IQueryable<FunctionReturnType> RecursiveQueryFunction(
        [Parameter(DbType = "boolean")] bool param1 = true
    )
    {
        //Example how to add parameters to your function
        //TODO: Ask how to make recursive queries with SQL 
        var param1 = new ObjectParameter("param1", param1);
        return this.ObjectContext().CreateQuery<FunctionReturnType>(
            $"RecursiveQueryFunction(@{nameof(param1)})", param1);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //add both (Function returntype and the actual function) to your modelbuilder. 
        modelBuilder.ComplexType<FunctionReturnType>();
        modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);

        base.OnModelCreating(modelBuilder);
    }

    public IEnumerable<Category> GetParents(Guid id)
    {
        //this = dbContext
        return from hierarchyRow in this.RecursiveQueryFunction(true)
            join yourClass from this.Set<YourClassThatHasHierarchy>()
            on hierarchyRow.Id equals yourClass.Id
            where hierarchyRow.AnchorId == id
            select yourClass;
    }
}
Ozzian
quelle
1

Ich habe herausgefunden, dass Sie die gesamte übergeordnete Hierarchie erhalten, wenn Sie "zwei übergeordnete Ebenen" einschließen:

var query = Context.Items
            .Include(i => i.Category)
            .Include(i => i.Category.Parent.Parent)
Francisco Cardoso
quelle
0

Versuche dies

List<SiteActionMap> list = this.GetQuery<SiteActionMap>()
                .Where(m => m.Parent == null && m.Active == true)
                .Include(m => m.Action)
                .Include(m => m.Parent).ToList();    

if (list == null)
    return null;

this.GetQuery<SiteActionMap>()
    .OrderBy(m => m.SortOrder)
    .Where(m => m.Active == true)
    .Include(m => m.Action)
    .Include(m => m.Parent)
    .ToList();

return list;
Tobias
quelle
0

@parliament gab mir eine Idee für EF6. Beispiel für eine Kategorie mit Methoden zum Laden aller Eltern bis zum Stammknoten und aller Kinder.

HINWEIS: Verwenden Sie diese Option nur für nicht leistungskritischen Betrieb. Beispiel mit 1000 Knoten Leistung von http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html .

Loading 1000 cat. with navigation properties took 15259 ms 
Loading 1000 cat. with stored procedure took 169 ms

Code:

public class Category 
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Name { get; set; }

    public int? ParentId { get; set; }

    public virtual Category Parent { get; set; }

    public virtual ICollection<Category> Children { get; set; }

    private IList<Category> allParentsList = new List<Category>();

    public IEnumerable<Category> AllParents()
    {
        var parent = Parent;
        while (!(parent is null))
        {
            allParentsList.Add(parent);
            parent = parent.Parent;
        }
        return allParentsList;
    }

    public IEnumerable<Category> AllChildren()
    {
        yield return this;
        foreach (var child in Children)
        foreach (var granChild in child.AllChildren())
        {
            yield return granChild;
        }
    }   
}
Ogglas
quelle
0

Mein Vorschlag wäre

var query = CreateQuery()
    .Where(entity => entity.Id == Id)
    .Include(entity => entity.Parent);
var result = await FindAsync(query);

return result.FirstOrDefault();

und es bedeutet, dass einzelne entityund alle diese entity.ParentEntitäten geladen werden recursive.

entity is same as entity.Parent
aursad
quelle
0
public static class EntityFrameworkExtensions
{
    public static ObjectContext GetObjectContext(this DbContext context) 
    {
        ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;

        return objectContext;
    }

    public static string GetTableName<T>(this ObjectSet<T> objectSet) 
        where T : class
    {
        string sql = objectSet.ToTraceString();
        Regex regex = new Regex("FROM (?<table>.*) AS");
        Match match = regex.Match(sql);

        string table = match.Groups["table"].Value;
        return table;
    }

    public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
        where T : class
    {
        var objectContext = context.GetObjectContext();

        var entityObjectSet = objectContext.CreateObjectSet<T>();
        var entityTableName = entityObjectSet.GetTableName();
        var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;

        var navigationProperty = entityObjectSet
            .EntitySet
            .ElementType
            .DeclaredNavigationProperties
            .Where(w => w.Name.Equals(navigationPropertyName))
            .FirstOrDefault();

        var association = objectContext.MetadataWorkspace
            .GetItems<AssociationType>(DataSpace.SSpace)
            .Single(a => a.Name == navigationProperty.RelationshipType.Name);

        var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
        var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;

        var sqlQuery = @"
                EXEC ('
                    ;WITH CTE AS
                    (
                        SELECT 
                            [cte1].' + @TABLE_PK + '
                            , Level = 1
                        FROM ' + @TABLE_NAME + ' [cte1]
                        WHERE [cte1].' + @TABLE_FK + ' IS NULL

                        UNION ALL

                        SELECT 
                            [cte2].' + @TABLE_PK + '
                            , Level = CTE.Level + 1
                        FROM ' + @TABLE_NAME + ' [cte2]
                            INNER JOIN CTE ON CTE.' + @TABLE_PK + ' = [cte2].' + @TABLE_FK + '
                    )
                    SELECT 
                        MAX(CTE.Level)
                    FROM CTE 
                ')
            ";

        var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
            {
                new SqlParameter("TABLE_NAME", entityTableName),
                new SqlParameter("TABLE_PK", pkName),
                new SqlParameter("TABLE_FK", fkName)
            });

        var includeCount = rawSqlQuery.FirstOrDefault();

        var include = string.Empty;

        for (var i = 0; i < (includeCount - 1); i++)
        {
            if (i > 0)
                include += ".";

            include += navigationPropertyName;
        }

        return query.Include(include);
    }
}
Hugo Brunholi
quelle
context.YourEntitySet.RecursiveInclude(i => i.YourIncludeProperty, context)
Hugo Brunholi
0

Lassen Sie mich meine einfache Lösung anbieten, die den Anforderungen entspricht, um den Zweig hierarchischer Daten der Organisationsstruktur der ausgewählten Abteilung zu aktivieren / deaktivieren.

Die Tabelle Abteilungen sieht nach dieser SQL aus

CREATE TABLE [dbo].[Departments](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](1000) NOT NULL,
    [OrganizationID] [int] NOT NULL,
    [ParentID] [int] NULL,
    [IsEnabled] [bit] NOT NULL, 
 CONSTRAINT [PK_Departments] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

C # -Code bietet einen sehr einfachen Ansatz, der für mich gut funktioniert. 1. Es gibt die vollständige Tabelle asynchron zurück. 2. Es ändert die Eigenschaft für die verknüpften Zeilen.

public async Task<bool> RemoveDepartmentAsync(int orgID, int depID)
            {
                try
                {
                    using (var db = new GJobEntities())
                    {
                        var org = await db.Organizations.FirstOrDefaultAsync(x => x.ID == orgID); // Check if  the organization exists
                        if (org != null)
                        {
                            var allDepartments = await db.Departments.ToListAsync(); // get all table items
                            var isExisting = allDepartments.FirstOrDefault(x => x.OrganizationID == orgID && x.ID == depID);
                            if (isExisting != null) // Check if the department exists
                            {
                                isExisting.IsEnabled = false; // Change the property of visibility of the department
                                var all = allDepartments.Where(x => x.OrganizationID == orgID && x.ID == isExisting.ID).ToList();
                                foreach (var item in all)
                                {
                                    item.IsEnabled = false;
                                    RecursiveRemoveDepartment(orgID, item.ID, ref allDepartments); // Loop over table data set to change property of the linked items
                                }
                                await db.SaveChangesAsync();
                            }
                            return true;
                        }
                    }
                }
                catch (Exception ex)
                {
                    logger.Error(ex);
                }

                return false;
            }

            private void RecursiveRemoveDepartment(int orgID, int? parentID, ref List<Department> items)
            {
                var all = items.Where(x => x.OrganizationID == orgID && x.ParentID == parentID);
                foreach (var item in all)
                {
                    item.IsEnabled = false;
                    RecursiveRemoveDepartment(orgID, item.ID, ref items);
                }
            }

Dieser Ansatz funktioniert sehr schnell für relativ kleine Datensätze, ich schätze weniger als 100000. Wahrscheinlich müssen Sie für große Datenmengen serverseitig gespeicherte Funktionen implementieren.

Genießen!

DmitryBoyko
quelle