Home > OS >  Best way to read and display data from nested classes/tables in Blazor
Best way to read and display data from nested classes/tables in Blazor

Time:01-15

Main Goal

I am creating a Blazor app that will display a single paragraph of report one-at-a-time. The user will need to click on some button to go to the next paragraph of the report. For this, 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 a 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 >    
    <button  
    @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());
    }
}

CodePudding user response:

I run an ESL website that has entire book texts broken down like this. I can say a few things:

  1. Unless you need to edit the paragraphs and / or sentences after you first register them, don't use pre-set tiers of organization. This will make things like searching the text more complex (not impossible, but more complex). Save only the full text to the DB, and use a marking system to determine how to break it down-- i.e. use a destructive system rather than a constructive one.

  2. This is a matter of taste, but I don't like keying data that's always going to be included in a parent. Better have something like:

.

public class Section
{
    public int Id { get; set; }
    public string Name { get; set; }
    List<paragraph> Paragraphs {get; set;}
}

This reduces duplication (filling a parent ID when the child nodes are part of the parent object anyway) and makes rendering your content trivially simple: .

@foreach (var paragraph in Section.Paragraphs){
    <ParagraphDisplay Content=paragraph />
}

Of course, your DB tables will have keys-- but the interfaces for you front end don't have to know about any of that unless you have a really good reason for doing so. Interfaces are as much about hiding inner workings as enforcing them.

CodePudding user response:

When you use Entity Framework, there's a fundamental data model design decision that you need to make:

Do I use the full functionality of EF or do I treat it as a simple Object Request Mapper [ORM].

Using EF to do it all seems a no brainer at first - it does much of the hard work for you.

That decision has consequences. You're building your Core/Application Domain logic - the relationships between your data objects - into your Infrastructure/Data Domain. You are welding your application to EF.

Once you've coded those relationships and EF has build the database structure on that design, changes and modifications can become problematic. The internals of how EF puts it all together is a bit of a black box for most people. While it all works, no problem.

The alternative approach, with more upfront code, is to use EF as a ORM and code the relationships into your Core/Application domain code.

For instance, you have an Application Domain Document object that you populate by fetching a DboDocument from the database through EF and separately fetching the associated DboSection collection. You build the complex object. It's more work, but you have control.

The decision is yours, but understand that you have made that design decision.

CodePudding user response:

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 recurssion 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 identificator, 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 >
        <button  @onclick="GotoNextCheckParagraph"> Next </button>
    </div>
} 
else 
{ 
    <p>End of Document.</p>

    <div >
        <button  @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 );
    } );            
}
  • Related