# Aggiungere una parte del sistema di commenti 1 - Alzare la base di dati

<!--category-- Entity Framework  -->
<datetime class="hidden">2024-08-26T11:53</datetime>

## Introduzione

Uno degli aspetti chiave di un sito di blog come questo è il sistema dei commenti. Quindi, ho deciso di costruirne unoM SK2 Questa è la prima parte di una serie di post su come costruire un sistema di commenti.

[TOC]

## Alzare la base di dati

Come con il resto del sito io' uso Postgres per i blog; per cominciare con l'IM SK2 sto usando quei queri ripetitivi per immagazzinare i commenti . mi dispiace molto questoMSC4 ma ci tira fuori la porta con un sistema di commento threaded MST5 e non hoMst6c'è poco trafficoM st7 quindiMSt8non è una gran cosaMstr9 IoMt10 saremo spostati verso un sistema più efficiente nel futuroMtr11

### Il contesto dell'EF

Nel sistema attuale usiamo un 'Comment Closure' per definire le relazioni tra i commenti - così posso supportare commenti threadedM SK3 Il nostro diagramma di classe assomiglia a questo

```mermaid
classDiagram
    class CommentEntity {
        int Id
        CommentStatus Status
        string Author
        string? HtmlContent
        string Content
        DateTime CreatedAt
        int PostId
        int? ParentCommentId
    }

    class BlogPostEntity {
        int Id
        string Title
        string Content
        DateTime CreatedAt
    }

    class CommentClosure {
        int AncestorId
        int DescendantId
        int Depth
    }

    CommentEntity "1" --> "1" BlogPostEntity : belongs to
    CommentEntity "1" --> "0..1" CommentEntity : parent
    CommentEntity "0..*" --> "0..*" CommentClosure : Ancestors
    CommentEntity "0..*" --> "0..*" CommentClosure : Descendants
    CommentClosure "1" --> "1" CommentEntity : Ancestor
    CommentClosure "1" --> "1" CommentEntity : Descendant

    class CommentStatus {
        <<enum>>
        Pending
        Approved
        Rejected
        Deleted
    }

```

Qui potete vedere che ogni `CommentEntity` ha una `PostId` per il posto sono attaccate alle stampe , e a `ParentCommentId` per il commento è 's attaccato a (se c'èM SK2 `CommentClosure` la tavola è usata per conservare le relazioni tra i commenti.

### Il CommentService

L'informazione figura nella parte dispositiva. `CommentService` è responsabile della gestione dei commenti. Ha metodi per aggiungereMSC1 approvare, rifiutareM SK3 e cancellare i commenti

L'interfaccia per questo servizio assomiglia a questa.

```csharp
public interface ICommentService
{
    Task<string> Add( int postId, int? parentCommentId, string author, string content);
    Task<List<CommentEntity>> GetForPost(int blogPostId, int page = 1, int pageSize = 10, int? maxDepth = null, CommentStatus? status = null);
    Task<List<CommentEntity>> GetDescendants(int commentId, int maxDepth = 0);

     Task<CommentEntity> Get(int commentId);
    Task<List<CommentEntity>> GetAncestors(int commentId);
    Task Delete(int commentId);
    Task Reject(int commentId);
    Task Approve(int commentId);
}
```

#### Il metodo "Add"

Il nostro metodo Add riassume la struttura di questo sistema di commenti.

Inizialmente inizia una nuova transazione; per quelle transazioni non familiari è un modo di assicurare che un gruppo di operazioni venga trattato come un'unica unità di lavoro. Se una delle operazioni fallisceM SK2 la transazione può essere riprodurreMSC3 e tutti i cambiamenti sono cancellatiMST4 Questo è importante quando avete molte operazioni che devono essere compiute insiemeMst5 e volete assicurarvi che tutte abbiano successo o falliscono insieme MST6

Poi analizza il testo dei commenti da Markdown in HTML usando la Markdig library. È un modo semplice per permettere agli utenti di formatare i loro commenti usando Markdown.

Poi crea l'entità e la conserva nella base di dati.

Questo è fatto in due fasi - prima il commento viene salvato, poi la chiusura del commento è salvataM SK2 Questo perché il commente deve essere salvato per generare l'IDMSC3 che viene usato nella chiusuraMNK4

Poi costruiamo la gerarchia di `CommentClosures` che definiscono le relazioni tra i commenti. Cominciamo con un auto-intry di riferimento alla chiusuraM SK2 poi, se c'è un commento parente, preleviamo tutti gli antenati del commento Parente e li aggiungiamo al nuovo commento.

Infine, commettiamo la transazione. Se una delle operazioni fallisceM SK2 l'operazione viene riprodurreMSC3

```csharp
 public async Task<string> Add(int postId, int? parentCommentId, string author, string content)
  {
      await using var transaction = await context.Database.BeginTransactionAsync();
      try
      {
         var html = Markdig.Markdown.ToHtml(content);
          // Create the new comment
          var newComment = new CommentEntity()
          {
              HtmlContent = html,
              Content = content,
              CreatedAt = DateTime.UtcNow,
              PostId = postId,
              Author = author,
              Status = CommentStatus.Pending,
              ParentCommentId = parentCommentId
          };
            
          context.Comments.Add(newComment);
          await context.SaveChangesAsync();
          logger.LogInformation("Saved comment to DB");// Save to generate the new comment's Id

          // Insert into CommentClosure table
          var commentClosures = new List<CommentClosure>();

          // Self-referencing closure entry
          commentClosures.Add(new CommentClosure
          {
              AncestorId = newComment.Id,
              DescendantId = newComment.Id,
              Depth = 0
          });

          // If there is a parent comment, insert the ancestor relationships
          if (parentCommentId.HasValue)
          {
              // Fetch all ancestors of the parent comment
              var parentAncestors = await context.CommentClosures
                  .Where(cc => cc.DescendantId == parentCommentId.Value)
                  .ToListAsync();

              // Add ancestor relationships for the new comment
              foreach (var ancestor in parentAncestors)
              {
                  commentClosures.Add(new CommentClosure
                  {
                      AncestorId = ancestor.AncestorId,
                      DescendantId = newComment.Id,
                      Depth = ancestor.Depth + 1
                  });
              }

              // Add a direct parent-child relationship
              commentClosures.Add(new CommentClosure
              {
                  AncestorId = parentCommentId.Value,
                  DescendantId = newComment.Id,
                  Depth = 1
              });
          }

          context.CommentClosures.AddRange(commentClosures);
          await context.SaveChangesAsync();
          logger.LogInformation("Saved comment closure to DB");

          // Commit transaction
          await transaction.CommitAsync();
          return html;
      }
      catch (Exception e)
      {
          // Rollback transaction in case of failure
          await transaction.RollbackAsync();
          logger.LogError(e, "Failed to save comment to DB");
      }

      return string.Empty;
  }

```

#### Il metodo GetForPost

Noi non ricopriremo l'intera cosa, ma `Add` e `Get` sono le nostre principali operazioni CRUD per questo servizio.

Come potete vedere abbiamo dei messaggi basati sul commento di alto livello. Abbiamo anche un `maxDepth` parametro che ci permette di limitare la profondità dell'albero dei commenti. Questo è utile se vogliamo mostrare solo i commenti a livello superioreM SK1, o se vogliamo limitarla per migliorare la performance.

Con questo approccio recursivo dobbiamo applicare il filtro di profondità dopo aver prelevato i commenti.

```csharp
  public async Task<List<CommentEntity>> GetForPost(int blogPostId, int page = 1, int pageSize = 10, int? maxDepth = null, CommentStatus? status = null)
  {
      // Step 1: Query the top-level comments for the specified blog post
      var query = context.Comments
          .Where(c => c.PostId == blogPostId)
          .OrderByDescending(c => c.CreatedAt)
          .Skip((page - 1) * pageSize)
          .Take(pageSize);

      // Step 2: Filter by status if provided
      if (status.HasValue)
      {
          query = query.Where(c => c.Status == status.Value);
      }

      var topLevelComments = await query
          .Include(c => c.ParentComment)
          .Include(d=>d.Descendants)
          .ToListAsync();

      // Step 4: Filter descendants based on the maxDepth
      foreach (var comment in topLevelComments)
      {
          if (maxDepth != null)
          {
              FilterDescendantsByDepth(comment, 0, maxDepth.Value);
          }
      }

      return topLevelComments;
  }

// Recursive helper method to limit the descendants based on the specified depth
  private void FilterDescendantsByDepth(CommentEntity comment, int currentDepth, int maxDepth)
  {
      if (currentDepth >= maxDepth)
      {
          // If the max depth is reached or there are no descendants, stop recursion
          comment.Descendants = new List<CommentClosure>();  // Clear further descendants beyond maxDepth
          return;
      }

      foreach (var closure in comment.Descendants.ToList())  // Iterate over a copy to prevent modification during iteration
      {
          FilterDescendantsByDepth(closure.Descendant, currentDepth + 1, maxDepth);
      }
  }

```

## Conclusione

Questo è un semplice sistema di commenti threaded che usa quei queri ricursivi per immagazzinare le relazioni tra i commenti. E'' non il sistema più efficienteM SK2 ma è ' semplice e funzionaMSC4 In futuroMST5 IMSSK6 coprerò gli aspetti frontali di questo sistemaMSV7 tanti HTMXMSP8 Alpine MSV9js M SV10 e Tailwind CSSM SV11

Fino a quel momento, lasciate il vostro commento qui sotto!