Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to read and display data from nested classes/tables in Blazor?

Main Goal

I am creating a Blazor app that will display a single paragraph of a report one at a time. The user will need to click on some button to go to the next paragraph of the report. I need to create a database with tables for paragraphs, section headers, and documents.

My question

My question is related to the database part and the way on how to read the data from the database. I have used EntityFramework and SQLite to create a database. I want to know if I have done it correctly or not.

For this example, I have created sample data of paragraphs and sections. The relationship of each paragraph to a section is collected in a separate table named SectionParagraphs. (The relationship case for Documents and sections is similar, and I have thus not included them in this example).

I have sketched a simple model in the figure below. So, basically, a document will consist of sections, and each section will consist of Paragraphs.

Data Model of nesting Report Model

I have included the codes below. Do you think this is good method? and what will happen if there are millions of paragraphs. I am worried that this code will be slow.

Code To make things simple, I am only going to show the latter three classes.

Here is Section.cs class.

public class Section
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

Here is Paragraph.cs class.

public class Paragraph
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
}

Here is relationship class SectionParagraph.cs class.

public class SectionParagraph
{
    public int ParagraphId { get; set; }
    public int SectionId { get; set; }
}

and, heere is DbContext class.

public class ReportContext : DbContext
{
    public DbSet<Paragraph> Paragraphs { get; set; } 
    public DbSet<Section> Sections { get; set; }
    public DbSet<SectionParagraph> SectionParagraphs { get; set; }

    public ReportContext(DbContextOptions<ReportContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<SectionParagraph>().HasKey(s=> new { s.SectionId, s.ParagraphId });
        mb.Entity<SectionParagraph>().HasOne<Section>().WithMany(e=> e.SectionParagraphs);

    }

}

Here is seedData.cs class.

public class SeedData
{
    public static void InitializeChecklist(ReportContext db)
    {
        List<Section> sections = new List<Section>();
        for(int i = 1; i < 11; i++)
        {
            sections.Add(new Section() { Id = i, Name = String.Format("Section {0}", i) });
        }

        List<Paragraph> paragraphs = new List<Paragraph>();
        for (int i = 1; i < 21; i++)
        {
            paragraphs.Add(new Paragraph() { Id = i, Name = String.Format("Paragraph No. {0}", i) });
        }
        SectionParagraph[] sectionParagraphs = new SectionParagraph[]
        {
            new SectionParagraph(){ SectionId = 1, ParagraphId = 1 },
            new SectionParagraph(){ SectionId = 2, ParagraphId = 2 },
            new SectionParagraph(){ SectionId = 3, ParagraphId = 3 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 4 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 5 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 6 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 7 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 8 },
            new SectionParagraph(){ SectionId = 4, ParagraphId = 9 }
        }

        db.SectionParagraphs.AddRange(sectionParagraphs);
        db.Sections.AddRange(sections);
        db.Paragraphs.AddRange(paragraphs);
        db.SaveChanges();
    }
}

and, here is the Index.razor

@page "/"
@using SampleBlazorApp.Model;
@inject HttpClient HttpClient;
@inject NavigationManager NavigationManager

<PageTitle>Index</PageTitle>

@if (paragraphs is null)
{
    <p>Loading....</p>
}
else
{
    if (_currentIndex < sectionParagraphs.Count)
    {
        <p><b>Section Id: @sections.Where(s => s.Id == sectionParagraph.SectionId).FirstOrDefault().Id</b></p>
        <p><b>Section Name: @sections.Where(s => s.Id == sectionParagraph.SectionId).FirstOrDefault().Name</b></p>

        <p>ParagraphNumber: @paragraphs.Where(s => s.Id == sectionParagraph.ParagraphId).FirstOrDefault().Id</p>
        <p>Paragraph Name:> @paragraphs.Where(s => s.Id == sectionParagraph.ParagraphId).FirstOrDefault().Name</p>
    }
    else
    {
        <p>End of Document.</p>
    }
}

<div class="dialog-buttons">    
    <button class="btn btn-secondary mr-auto" 
    @onclick="GotoNextCheckParagraph">Next</button>
</div>


@code
{
    int _currentIndex = 0;    
    SectionParagraph sectionParagraph = new();

    List<Paragraph> paragraphs;
    List<SectionParagraph> sectionParagraphs;
    List<Section> sections;

    protected override async Task OnInitializedAsync()
    {
        paragraphs = await HttpClient.GetFromJsonAsync<List<Paragraph>>(NavigationManager.BaseUri + "paragraphs");
        sectionParagraphs = await HttpClient.GetFromJsonAsync<List<SectionParagraph>>(NavigationManager.BaseUri + "sectionparagraphs");
        sections = await HttpClient.GetFromJsonAsync<List<Section>>(NavigationManager.BaseUri + "sections");
        
        sectionParagraph = sectionParagraphs[0];
        section = sections[0];
        paragraph = paragraphs[0];
    }

    private void GotoNextCheckParagraph()
    {
        // Read next paragraph //
        _currentIndex++;
        if (_currentIndex < sectionParagraphs.Count)
        {
            sectionParagraph = sectionParagraphs[_currentIndex];
        }
    }
}

and one of the boilerplate Controller class looks like this.

[Route("sections")]
[ApiController]
public class SectionController
{
    private readonly ReportContext _db;

    public SectionController(ReportContext db)
    {
        _db = db;
    }

    [HttpGet]
    public async Task<ActionResult<List<Section>>> GetSections()
    {
        return await (_db.Sections.ToListAsync());
    }
}
like image 376
moostang Avatar asked Oct 23 '25 14:10

moostang


1 Answers

Since relationship between your models is one-to-many, there is no need to add a connecting model between them, I agree with @Bennyboy1973 that you could just fully define relationships by updating your models like this:

public class Section
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    public IList<Paragraph> Paragraphs { get; set; }
}

public class Paragraph
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    public int SectionId { get; set; } // FK

    [JsonIgnore] // to avoid recursion in the JSON response
    public Section Section { get; set; }
}

Then, the JSON output would look like this:

[
  {
    "id": 1,
    "name": "Section1",
    "paragraphs": [
      {
        "id": 1,
        "name": "Par1",
        "sectionId": 1
      },
      {
        "id": 2,
        "name": "Par2",
        "sectionId": 1
      }
      . . .
    ]
  }
  . . .
]

This design will lessen the code in the razor, because you will only need to call a sections (for your current situation) like this:

List<Section>? sections;

protected override async Task OnInitializedAsync()
{
    sections = await GetSectionsAsync();
}

As you requested, you want to go to the next paragraph inside current section on button click. To achieve this, I've created another local index identifier, specially for paragraphs inside the Paragraphs collection of the particular Section:

int _currentParaIndex = 0;

To switch between views when we have something to show and the end of the document, I've added another local variable that defines the state:

bool _dataExists = true;

Now, when we have all the information in one List<Section> sections collection and all the variables to control the view, we can get all the information we want to display from there like this:

if (_dataExists)
{
    var currentSection = sections[_currentSectionIndex];

    <p><b>Section Id:</b> @(currentSection.Id) </p>
    <p><b>Section Name:</b> @(currentSection.Name) </p>

    <p><b>Paragraph Id:</b> @(currentSection.Paragraphs[_currentParaIndex].Id) </p>
    <p><b>Paragraph Name:</b> @(currentSection.Paragraphs[_currentParaIndex].Name) </p>

    <div class="dialog-buttons">
        <button class="btn btn-secondary mr-auto" @onclick="GotoNextCheckParagraph"> Next </button>
    </div>
} 
else 
{ 
    <p>End of Document.</p>

    <div class="dialog-buttons">
        <button class="btn btn-secondary mr-auto" @onclick="Reset"> Reset </button>
    </div>
}

And, finally, the logic for the GotoNextCheckParagraph looks like this:

private void GotoNextCheckParagraph()
{
    _currentParaIndex++; // Read next paragraph

    if (_currentParaIndex == sections?[_currentSectionIndex].Paragraphs.Count) // Check if the current paragraph index is the same
                                                                                // as the amount of paragraphs in the current section
    {
        _currentParaIndex = 0; // Reset index to zero (first element) to avoid exception
        _currentSectionIndex++; // Read next section

        if (_currentSectionIndex == sections.Count) // Check if the current section index (last) is the same
                                                     // as the amount of paragraphs in the current section
        {
            _dataExists = false; // If we got here, it means that we have checked every paragraph of every section
                                 // Therefore, inform that we've reached the end of the doc
        }
    }
}

private void Reset()
{
    _currentParaIndex = 0;
    _currentSectionIndex = 0;
    _dataExists = true;
}

Also, now you would need to check for sections instead of paragraphs at the top:

@if( sections is null )
{
    <p>Loading....</p>
}
else
{
    . . .
}

I hope it helps!

UPD-1:

I forgot to mention that with the new design, your API method should look like this:

[HttpGet]
public async Task<ActionResult<IEnumerable<Section>>> GetSections()
{
    var sections = await _context.Sections
                     .Include(s => s.Paragraphs)
                     .ToListAsync();

    return sections;
}

UPD-2:

And DataContext:

public DbSet<Section> Sections { get; set; }
public DbSet<Paragraph> Paragraphs { get; set; }

protected override void OnModelCreating( ModelBuilder modelBuilder )
{
    base.OnModelCreating( modelBuilder );

    modelBuilder
        .Entity<Section>()
        .ToTable( nameof( Section ) )
        .HasKey( x => x.Id );

    modelBuilder.Entity<Paragraph>( entity =>
    {
        entity.ToTable( nameof( Paragraph ) );
        entity.HasOne( p => p.Section )
            .WithMany( s => s.Paragraphs )
            .HasForeignKey( p => p.SectionId )
            .OnDelete( DeleteBehavior.Cascade );
    } );            
}
like image 136
desmondische Avatar answered Oct 26 '25 05:10

desmondische



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!