# Datahierarkier Del 1.5: PostgreSQL ltree med EF Core

<!--category-- Entity Framework, PostgreSQL, EF Hierarchies -->
<datetime class="hidden">2025-12-06T09:50</datetime>

PostgreSQL: s ltree förlängning ger dig materialiserade vägar med databas-infödda superkrafter: GiST index, specialiserade operatörer som `@>` och `<@`, och kraftfull mönster matchning. Om du är engagerad i PostgreSQL och vill ha den bästa hierarkin frågeprestanda, är ltree svårt att slå.

**Goda nyheter:** och [Npgsql EF Core leverantör stöder LINQ översättningar för ltree verksamhet](https://www.npgsql.org/efcore/mapping/translations.html#ltree-functions) via `LTree` typ. Du kan använda metoder som `IsAncestorOf()`, `IsDescendantOf()`, och `MatchesLQuery()` direkt i LINQ-frågor. EF Core stöder dock ännu inte rekursiva CTE, så du behöver rå SQL för operationer som kräver dem (som att bygga kompletta delträd resultat med beräknade djup).

*Tack till [Shay Rojansky Ordförande](mailto:roji@roji.org) för att peka ut LINQ översättning stöd!*

## Serienavigering

- [Del 1: Översikt](/blog/efcore-hierarchical-data) - Inledning och jämförelse
- [Del 1.1: Tilläggslista](/blog/efcore-hierarchical-data-adjacency)
- [Del 1.2: Stängningstabell](/blog/efcore-hierarchical-data-closure)
- [Del 1.3: Materialiserad väg](/blog/efcore-hierarchical-data-path)
- [Del 1.4: Inhägnade satser](/blog/efcore-hierarchical-data-nested)
- **Del 1.5: Ltree** (denna artikel)

---


## Vad är Itree?

[`ltree`](https://www.postgresql.org/docs/current/ltree.html) är en PostgreSQL- förlängning som ger en infödd datatyp för hierarkiska etikettsökvägar. Tänk på det som [Materialiserad väg](/blog/efcore-hierarchical-data-path) med supermakter - databasen förstår strukturen och ger optimerade operatörer, funktioner och GiST indexstöd.

Istället för att behandla sökvägen som en dum sträng och använda LIKA frågor, PostgreSQL kan:

- Använd specialiserade operatörer (`@>` för "är förfader till", `<@` för "är ättling till")
- Tillämpa GiST-index för effektiva hierarkifrågor
- Matcha mönster med jokertecken (`Top.*.Europe`)
- Utför inställda åtgärder på sökvägar

**Nyckelinsikt:** Ltree är det bästa av båda världarna - enkelheten i materialiserade vägar med databas-nativ optimering. Avvecklingen är PostgreSQL-låsning, och medan många ltree verksamhet arbetar via LINQ, recursive CTEs fortfarande kräver rå SQL.

[TOC]

## Format för ltree- sökväg

Sökvägar i ltree använder perioder som avskiljare och alfanumeriska etiketter:

```
Top.Countries.Europe.UK
Top.Countries.Asia.Japan.Tokyo
Top.Products.Electronics.Computers.Laptops
```

Regler:

- Etiketter kan innehålla bokstäver, siffror och understreck
- Etiketter är skiftlägeskänsliga
- Maximal etikettlängd är 256 tecken
- Maximal gånglängd är 65535 etiketter

För kommentarsystem skulle vi använda ID som etiketter: `1.3.7` vilket betyder "kommentar 7 under kommentar 3 under kommentar 1".

## Sätta upp ltree

För det första, aktivera utökningen (kräver databas superuser-privilegier):

```sql
CREATE EXTENSION IF NOT EXISTS ltree;
```

Eller via EF Core migration:

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS ltree");
}
```

## Enhetsdefinition

Leverantören av Npgsql innehåller en `LTree` skriva som kartor direkt till PostgreSQL: s ltree och ger LINQ-översättbara metoder:

```csharp
using Microsoft.EntityFrameworkCore;

public class Comment
{
    public int Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }

    public int PostId { get; set; }
    public BlogPost Post { get; set; } = null!;

    // ========== LTREE PATH ==========

    // The hierarchical path in ltree format
    // Format: ancestor1.ancestor2.thisNode
    // Examples:
    //   Root comment: "1"
    //   Child of 1: "1.5"
    //   Grandchild: "1.5.12"
    //
    // Using the LTree type enables LINQ translations for ltree operators
    public LTree Path { get; set; }

    // Keep ParentCommentId for convenience
    public int? ParentCommentId { get; set; }
    public Comment? ParentComment { get; set; }
    public ICollection<Comment> Children { get; set; } = new List<Comment>();

    // ========== HELPER METHODS ==========

    // Helper to get depth - LTree has NLevel property for this
    public int GetDepth() => Path.NLevel - 1;

    public IEnumerable<int> GetAncestorIds()
    {
        var pathString = Path.ToString();
        if (string.IsNullOrEmpty(pathString)) yield break;

        var parts = pathString.Split('.');
        // All except last (which is this node)
        for (int i = 0; i < parts.Length - 1; i++)
        {
            if (int.TryParse(parts[i], out var id))
                yield return id;
        }
    }
}
```

## EF- kärninställning

```csharp
public class CommentConfiguration : IEntityTypeConfiguration<Comment>
{
    public void Configure(EntityTypeBuilder<Comment> builder)
    {
        builder.HasKey(c => c.Id);

        builder.Property(c => c.Content)
            .IsRequired()
            .HasMaxLength(10000);

        builder.Property(c => c.Author)
            .IsRequired()
            .HasMaxLength(200);

        // ========== PATH COLUMN ==========
        // The LTree type is automatically mapped to PostgreSQL's ltree type
        // by the Npgsql provider - no explicit column type needed
        builder.Property(c => c.Path)
            .IsRequired();

        // Relationship to blog post
        builder.HasOne(c => c.Post)
            .WithMany(p => p.Comments)
            .HasForeignKey(c => c.PostId)
            .OnDelete(DeleteBehavior.Cascade);

        // Self-referencing
        builder.HasOne(c => c.ParentComment)
            .WithMany(c => c.Children)
            .HasForeignKey(c => c.ParentCommentId)
            .OnDelete(DeleteBehavior.Restrict);

        // Standard indexes
        builder.HasIndex(c => c.PostId);
        builder.HasIndex(c => c.ParentCommentId);
    }
}
```

Lägg till GiST-indexet via migrering:

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // GiST index for ltree - enables efficient @>, <@, and ~ operators
    migrationBuilder.Sql(
        "CREATE INDEX ix_comments_path_gist ON comments USING GIST (path)");

    // Alternative: B-tree index for exact match and sorting
    // migrationBuilder.Sql(
    //     "CREATE INDEX ix_comments_path_btree ON comments USING BTREE (path)");
}
```

## ltree Operatörer

ltree tillhandahåller kraftfulla operatörer. Leverantören av Npgsql EF Core översätter `LTree` Metoder för dessa operatörer:

på Operatör  på vilket sätt  på LINQ-metoden  på SQL-exemplet
|----------|---------|-------------|-------------|
| `@>` på är förfader till (innehåller)  på `ltree1.IsAncestorOf(ltree2)` | `'1.3'::ltree @> '1.3.7'::ltree` → sant
| `<@` på är ättling till (som ingår i)  på `ltree1.IsDescendantOf(ltree2)` | `'1.3.7'::ltree <@ '1.3'::ltree` → sant
| `~` Motsvarande lquery-mönster `ltree.MatchesLQuery(pattern)` | `'1.3.7'::ltree ~ '1.*'::lquery` → sant
| `@` Matchar ltxtquery `ltree.MatchesLTxtQuery(query)` | `'1.3.7'::ltree @ '3 & 7'::ltxtquery` → sant
| `||` på konkatenterade vägar  och (använd strängkonkatering) `'1.3'::ltree || '7'::ltree` → '1.3.7' |
| `<`, `>`, `<=`, `>=` på grundval av de uppgifter som lämnats av de behöriga myndigheterna i den berörda medlemsstaten.

Ytterligare LINQ-translaterbara egenskaper och metoder:

- `ltree.NLevel` → `nlevel(ltree)` - antal etiketter i sökväg
- `ltree.Subtree(start, end)` → `subltree(ltree, start, end)` - Utdragssortiment av etiketter
- `ltree.Subpath(offset)` → `subpath(ltree, offset)` - Suffix från offset
- `ltree.Subpath(offset, len)` → `subpath(ltree, offset, len)` - delsträng
- `ltree.Index(subpath)` → `index(ltree, subpath)` - hitta subpath position
- `LTree.LongestCommonAncestor(ltree1, ltree2)` → `lca(ltree1, ltree2)` - lägsta gemensamma förfader

## Verksamhet

### Lägg till en ny kommentar

```csharp
public async Task<Comment> AddCommentAsync(
    int postId,
    int? parentId,
    string author,
    string content,
    CancellationToken ct = default)
{
    string path;

    if (parentId.HasValue)
    {
        // Get parent's path
        var parentPath = await context.Comments
            .Where(c => c.Id == parentId.Value)
            .Select(c => c.Path)
            .FirstOrDefaultAsync(ct);

        if (parentPath == null)
            throw new InvalidOperationException($"Parent comment {parentId} not found");

        // Create comment first to get the ID
        var comment = new Comment
        {
            PostId = postId,
            ParentCommentId = parentId,
            Author = author,
            Content = content,
            CreatedAt = DateTime.UtcNow,
            Path = string.Empty  // Temporary
        };

        context.Comments.Add(comment);
        await context.SaveChangesAsync(ct);

        // Build path: parentPath.newId
        // ltree uses periods as separators
        comment.Path = $"{parentPath}.{comment.Id}";
        await context.SaveChangesAsync(ct);

        logger.LogInformation("Added comment {CommentId} with ltree path {Path}",
            comment.Id, comment.Path);
        return comment;
    }
    else
    {
        // Root comment - path is just the ID
        var comment = new Comment
        {
            PostId = postId,
            ParentCommentId = null,
            Author = author,
            Content = content,
            CreatedAt = DateTime.UtcNow,
            Path = string.Empty
        };

        context.Comments.Add(comment);
        await context.SaveChangesAsync(ct);

        comment.Path = comment.Id.ToString();
        await context.SaveChangesAsync(ct);

        return comment;
    }
}
```

### Skaffa barn omedelbart

Använda ParentCommentId (enkel) eller ltree mönster matchning:

```csharp
public async Task<List<Comment>> GetChildrenAsync(int commentId, CancellationToken ct = default)
{
    // Option 1: Simple ParentCommentId lookup
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.ParentCommentId == commentId)
        .OrderBy(c => c.CreatedAt)
        .ToListAsync(ct);
}

// Option 2: Using ltree pattern (demonstration)
public async Task<List<Comment>> GetChildrenLtreeAsync(int commentId, CancellationToken ct = default)
{
    // Get parent path first
    var parentPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (parentPath == null)
        return new List<Comment>();

    // Children match pattern: parentPath.*{1}
    // The {1} means exactly one more label (immediate children only)
    var sql = @"
        SELECT * FROM comments
        WHERE path ~ ($1 || '.*{1}')::lquery
        ORDER BY created_at";

    return await context.Comments
        .FromSqlRaw(sql, parentPath)
        .AsNoTracking()
        .ToListAsync(ct);
}
```

### Få alla förfäder

Användning av LINQ med `IsAncestorOf` metod (översätts till `@>` verksamhetsutövare:

```csharp
public async Task<List<Comment>> GetAncestorsAsync(int commentId, CancellationToken ct = default)
{
    var targetPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (targetPath == default)
        return new List<Comment>();

    // Find all nodes whose path is an ancestor of this path
    // Using IsAncestorOf which translates to @> operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsAncestorOf(targetPath) && c.Id != commentId)
        .OrderBy(c => c.Path.NLevel)
        .ToListAsync(ct);
}
```

### Hämta alla descendanter

Användning av LINQ med `IsDescendantOf` metod (översätts till `<@` verksamhetsutövare:

```csharp
public async Task<List<Comment>> GetDescendantsAsync(int commentId, CancellationToken ct = default)
{
    var parentPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (parentPath == default)
        return new List<Comment>();

    // Find all nodes whose path is a descendant of this path
    // Using IsDescendantOf which translates to <@ operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(parentPath) && c.Id != commentId)
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}
```

### Få descendanter till maximalt djup

Användning av LINQ med `NLevel` För djupbegränsning:

```csharp
public async Task<List<Comment>> GetDescendantsToDepthAsync(
    int commentId,
    int maxDepth,
    CancellationToken ct = default)
{
    var comment = await context.Comments
        .FirstOrDefaultAsync(c => c.Id == commentId, ct);

    if (comment == null)
        return new List<Comment>();

    var basePath = comment.Path;
    var baseLevel = comment.Path.NLevel;

    // NLevel property translates to nlevel() function
    // Filter descendants within maxDepth levels
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(basePath) 
                 && c.Id != commentId
                 && c.Path.NLevel - baseLevel <= maxDepth)
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

// If you need the depth value in results, you can project it:
public async Task<List<CommentWithDepth>> GetDescendantsWithDepthAsync(
    int commentId,
    int maxDepth,
    CancellationToken ct = default)
{
    var comment = await context.Comments
        .FirstOrDefaultAsync(c => c.Id == commentId, ct);

    if (comment == null)
        return new List<CommentWithDepth>();

    var basePath = comment.Path;
    var baseLevel = comment.Path.NLevel;

    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(basePath) 
                 && c.Id != commentId
                 && c.Path.NLevel - baseLevel <= maxDepth)
        .OrderBy(c => c.Path)
        .Select(c => new CommentWithDepth
        {
            Id = c.Id,
            Content = c.Content,
            Author = c.Author,
            CreatedAt = c.CreatedAt,
            PostId = c.PostId,
            ParentCommentId = c.ParentCommentId,
            Path = c.Path.ToString(),
            Depth = c.Path.NLevel - baseLevel
        })
        .ToListAsync(ct);
}
```

### Mönstermatchande frågor

ltree stöder kraftfulla lquery mönster. Använd `MatchesLQuery` I LINQ:

```csharp
// Find all comments at exactly depth 2 under comment 1
public async Task<List<Comment>> GetAtDepthAsync(int commentId, int depth, CancellationToken ct = default)
{
    var path = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (path == default) return new List<Comment>();

    // Pattern: path.*{depth} matches exactly 'depth' more levels
    var pattern = $"{path}.*{{{depth}}}";
    
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.MatchesLQuery(pattern))
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

// Find all paths matching a pattern like "1.*.7" (any path through 1 ending in 7)
public async Task<List<Comment>> MatchPatternAsync(string pattern, CancellationToken ct = default)
{
    // MatchesLQuery translates to the ~ operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.MatchesLQuery(pattern))
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}
```

### Ta bort ett underträd

Du kan använda LINQ för att välja underträdet och sedan ta bort:

```csharp
public async Task DeleteSubtreeAsync(int commentId, CancellationToken ct = default)
{
    var path = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (path == default)
        throw new InvalidOperationException($"Comment {commentId} not found");

    // Delete all descendants (nodes where path is descendant of this path)
    // Note: ExecuteDeleteAsync requires EF Core 7+
    var deleted = await context.Comments
        .Where(c => c.Path.IsDescendantOf(path))
        .ExecuteDeleteAsync(ct);

    logger.LogInformation("Deleted {Count} comments with path prefix {Path}", deleted, path);
}
```

### Flytta ett underträd

ltree tillhandahåller funktioner för att hjälpa till med vägmanipulation:

```csharp
public async Task MoveSubtreeAsync(
    int commentId,
    int newParentId,
    CancellationToken ct = default)
{
    await using var transaction = await context.Database.BeginTransactionAsync(ct);

    try
    {
        var node = await context.Comments.FirstOrDefaultAsync(c => c.Id == commentId, ct);
        var newParent = await context.Comments.FirstOrDefaultAsync(c => c.Id == newParentId, ct);

        if (node == null || newParent == null)
            throw new InvalidOperationException("Node or parent not found");

        // Prevent cycles
        if (newParent.Path.StartsWith(node.Path))
            throw new InvalidOperationException("Cannot move under own descendant");

        var oldPath = node.Path;
        var newPath = $"{newParent.Path}.{node.Id}";

        // Update all descendants: replace old path prefix with new one
        // subpath(path, nlevel(oldPath)) gets the suffix after oldPath
        // We concatenate newPath with that suffix
        var sql = @"
            UPDATE comments
            SET path = $2::ltree || subpath(path, nlevel($1::ltree))
            WHERE path <@ $1::ltree";

        await context.Database.ExecuteSqlRawAsync(
            sql,
            new object[] { oldPath, newPath },
            ct);

        // Update parent reference
        node.ParentCommentId = newParentId;
        await context.SaveChangesAsync(ct);

        await transaction.CommitAsync(ct);

        logger.LogInformation("Moved subtree from {OldPath} to {NewPath}", oldPath, newPath);
    }
    catch
    {
        await transaction.RollbackAsync(ct);
        throw;
    }
}
```

## referens för ltree-funktioner

PostgreSQL ger många användbara ltree funktioner:

på funktion  på beskrivning  på exempel 
|----------|-------------|---------|
| `nlevel(ltree)` Antal etiketter `nlevel('1.3.7')` → 3 |
| `subpath(ltree, offset)` och Suffix från offset `subpath('1.3.7', 1)` → '3.7' |
| `subpath(ltree, offset, len)` Kopia till textning `subpath('1.3.7', 1, 1)` → '3' |
| `subltree(ltree, start, end)` på etiketternas bredd `subltree('1.3.7', 0, 2)` → '1.3' |
| `lca(ltree, ltree)` Den lägsta gemensamma förfadern `lca('1.3.7', '1.3.9')` → '1.3' |
| `text2ltree(text)` Omvandla text till ltree  på engelska `text2ltree('1.3.7')` |
| `ltree2text(ltree)` Omvandla tree till text `ltree2text('1.3.7'::ltree)` |

## Visualisering av frågeflöde

```mermaid
sequenceDiagram
    participant App as Application
    participant EF as EF Core
    participant PG as PostgreSQL + ltree

    Note over App,PG: Getting Descendants (GiST index)
    App->>EF: GetDescendantsAsync(commentId)
    EF->>PG: SELECT path FROM comments WHERE id = @id
    PG-->>EF: Path "1.3"
    EF->>PG: SELECT * FROM comments WHERE path <@ '1.3'::ltree
    Note over PG: Uses GiST index - O(log n)
    PG-->>EF: All descendants
    EF-->>App: List<Comment>

    Note over App,PG: Pattern Match Query
    App->>EF: MatchPatternAsync("1.*.7")
    EF->>PG: SELECT * FROM comments WHERE path ~ '1.*.7'::lquery
    Note over PG: GiST index supports pattern matching
    PG-->>EF: Matching comments
    EF-->>App: List<Comment>
```

## Prestandaegenskaper

på drift  på komplexitet  och anmärkningar  på
|-----------|------------|-------|
Ange bara sökvägssträngen
på Få barn  på O(1)  på mönstermatchning med GiST-index
och få förfäderna  på O(1)  på @> operatör med GiST-index
på Få ättlingar  på O(1)  och <@ operatör med GiST-index
till Mönstermatchning O(log n)  på GiST-index stöder Lquery  och
och flytta delträdet  på O(s)  och uppdatera ättlingsvägar
och ta bort delträdet  på O(1)  och <@ operator för urval

Med GiST index är ltree frågor extremt effektiva - typiskt O(log n) oavsett träddjup.

## Fördelar och nackdelar

VÄLKOMMEN TILL FÖRMÅN FÖR FÖRVÄRVSPRODUKTER
|------|------|
på databas-nativ optimering  på PostgreSQL-enbart
på GiST-index för alla hierarkiska frågor  på Extensionsberoende
En kraftfull mönstermatchning  på etiketter som är begränsade till alfanumeriska
till Inbyggd väg manipulation funktioner  på Recursive CTEs kräver rå SQL  och
Färre portabla än rena EF Core-lösningar
Kompakt lagring
Stöd för LINQ via Npgsqls `LTree` Typ  på vilken typ som helst, t.ex.

## När du ska använda ltree

**Välj ltree när:**

- Du är engagerad i PostgreSQL
- Prestanda är avgörande för hierarkiska frågor
- Du behöver mönstermatchning (hitta alla X.*.Y vägar)
- Du vill ha det bästa av materialiserade vägar
- Du vill ha LINQ-stöd för de flesta hierarkiska operationer

**Undvik ltree när:**

- Du behöver databasportabilitet (SQL Server, MySQL, etc.)
- Ditt team är obekant med PostgreSQL-tillägg
- Etiketter behöver icke-alfanumeriska tecken
- Du behöver rekursiva CTE och vill undvika rå SQL

## Jämförelse med materialiserad väg

"Aspekt  på materialiserad väg" "ltree"
|--------|-------------------|-------|
på indextyp  på B-trädet (endast förfix)  på GiST (alla mönster)
till Mönstermatchning  på samma sätt som "prefix%" endast  med fullständiga jokertecken
till Operatörer  på String jämförelse  på Native @>, <@, ~  till
till Portabilitet  på alla databaser  på PostgreSQL endast
Stöd för EF Core  med full LINQ  på LINQ via `LTree` typ (CTE:er behöver rå SQL)
Bra med index  och bra med GiST
på funktioner ingen (manuell tolkning)  på rikt funktionsbibliotek

## Exempel: Full kommentar Tree Query

Sätta ihop allt - få en hel kommentar träd med djup för ett blogginlägg:

```csharp
public async Task<List<CommentTreeItem>> GetPostCommentTreeAsync(
    int postId,
    int maxDepth = 5,
    CancellationToken ct = default)
{
    // Get all comments for the post with calculated depth
    // nlevel() counts the labels in the path
    var sql = @"
        WITH root_comments AS (
            -- Find root comments for this post (no dot in path = root)
            SELECT path, nlevel(path) as root_level
            FROM comments
            WHERE post_id = $1 AND path !~ '*.*'
        )
        SELECT
            c.id,
            c.content,
            c.author,
            c.created_at,
            c.post_id,
            c.parent_comment_id,
            c.path::text as path,
            nlevel(c.path) - COALESCE(
                (SELECT root_level FROM root_comments r
                 WHERE c.path <@ r.path
                 ORDER BY nlevel(r.path) DESC LIMIT 1),
                nlevel(c.path)
            ) as depth
        FROM comments c
        WHERE c.post_id = $1
          AND nlevel(c.path) <= $2 + 1  -- +1 because depth is 0-indexed
        ORDER BY c.path";  -- Perfect depth-first order!

    return await context.Database
        .SqlQueryRaw<CommentTreeItem>(sql, postId, maxDepth)
        .ToListAsync(ct);
}

public class CommentTreeItem
{
    public int Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public int PostId { get; set; }
    public int? ParentCommentId { get; set; }
    public string Path { get; set; } = string.Empty;
    public int Depth { get; set; }
}
```

## Serienavigering

- [Del 1: Översikt](/blog/efcore-hierarchical-data)
- [Del 1.1: Tilläggslista](/blog/efcore-hierarchical-data-adjacency)
- [Del 1.2: Stängningstabell](/blog/efcore-hierarchical-data-closure)
- [Del 1.3: Materialiserad väg](/blog/efcore-hierarchical-data-path)
- [Del 1.4: Inhägnade satser](/blog/efcore-hierarchical-data-nested)
- **Del 1.5: Ltree** (denna artikel)

## Vad är nästa?

Denna serie har omfattat fem tillvägagångssätt för hierarkiska data med hjälp av EF Core. Del 2 kommer att utforska att använda rå SQL och Dapper för ännu mer kontroll över hierarkiska frågor - kommer snart!