# Een commentaarsysteem-onderdeel toevoegen 1 - Opstelling van de database

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

## Inleiding

Eén van de belangrijkste aspecten van een blogsite als deze is een commentaarsysteem. Ik besloot er een te bouwen. Dit is het eerste deel van een reeks posten over hoe je een commentatuursysteem moet bouwen.

[TOC]

## Opstelling van de Databasis

Zoals bij de rest van de site gebruik ik Postgres voor de blogs; om te beginnen met I' gebruik ik recursieve zoekopdrachten om de opmerkingen op te slaan . Ik vind dit erg ongemakkelijkM SK4 maar het haalt ons uit het raam met een gedraadde commentaarsysteem.

### De EF-context

In het huidige systeem gebruiken we een 'Komment Closure' om de relaties tussen de opmerkingen te definiëren.

```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
    }

```

Je kunt hier zien dat elke `CommentEntity` heeft een `PostId` Voor de post zijn het's aanhangen aan, en een `ParentCommentId` Voor de commentaar is het 'aanhangend aan (indien er een is).. `CommentClosure` Tabel wordt gebruikt om de relaties tussen opmerkingen te slaan.

### De CommentService

De gemeenschappelijke wetgeving van het land. `CommentService` Het is verantwoordelijk voor het beheren van opmerkingen. Het heeft methodes om aan te voegen , goedkeuring, afwijzenM SK3 en commentatjes te verwijderenMSC4 Het kent ook methodes voor het krijgen van commentaar voor een postMST5 en het krijgen vonden voor een commentaarMst6

Onze interface voor deze dienst ziet er zo uit:

```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);
}
```

#### De Voeg-Methode

Onze Add-methode vat de structuur van dit commentaarsysteem samen.

Het begint eerst met een nieuwe transactie; voor de niet bekende transacties zijn een manier om ervoor te zorgen dat een groep operaties behandeld wordt als één enkele werkeenheid . Als één van de operaties mislukt, kan de transactie teruggedraaid wordenM SK3 en alle veranderingen worden ongedaan gezetMSC4 Dit is belangrijk wanneer je meerdere operaties hebt die samen moeten worden gedaanMST5 en je wilt ervoor zorgen dat ze allemaal samen succesvol of mislukkenMSP6

Het berekent dan de commentaartekst van Markdown naar HTML met behulp van de Markdig bibliotheek. Dit is een eenvoudige manier om gebruikers hun opmerkingen te laten formateren met Markdown.

Dan creëert het de entiteit en bewaart dat in de database.

Dit gebeurt in twee stappen: - eerst wordt de commentaar gestopt, dan is de commentaire afsluiting gestopt\. Dit komt omdat het commentaar bewaard moet worden om de Id te genereren.

Dan bouwen we de hiërarchie van `CommentClosures` Die definiëren de relaties tussen de commentaties. We beginnen met een zelf-relatief afsluit-inskrywingM SK2 dan als er een ouderaanmerking is, halen we alle voorouders van de ouderaanhanging en voegen ze toe aan de nieuwe aanmerking.

Uiteindelijk, doen we de transactie af. Als een van de operaties mislukt, is de transaction teruggedraaid.

```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;
  }

```

#### De GetForPost-methode

We zullen de hele zaak niet behandelen, maar `Add` en `Get` zijn onze belangrijkste CRUD-operaties voor deze dienst.

Zoals je ziet hebben we een paging gebaseerd op de toplevel commentaar. We hebben ook een `maxDepth` parameter waarmee we de diepte van de commentaarboom kunnen beperken.. Dit is bruikbaar als we alleen de bovenste comments willen laten zien.

Onverwacht met deze recursieve aanpak moeten we de dieptefilter toepassen nadat we de opmerkingen hebben gehaald, zoals we dat kunnen doen in de zoekopdracht. Dit komt omdat de diepe van een commentaar bepaald wordt door het aantal voorouders dat hij heeft, en we dit niet makkelijk kunnen in SQL vragen.

```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);
      }
  }

```

## In slotting:

Dit is een eenvoudig getrainde commentaarsysteem dat recursieve zoekopdrachten gebruikt om de relaties tussen reacties op te slaan.. Het is niet het meest efficiënte systeem, maar het is simpel en werkt.

Tot dan toe, kan je een commentaar achterlaten onder