Search code examples
c#entity-frameworkrazorrelationship.net-7.0

How to establish n:1-relation with Razor/Entity Framework


I'm working with VS 2022 for Mac 17.6. My current project is Razor web app with .NET 7.0 and Entity Framework with SQLite.

This is what I want to achieve: in my data model I have, among others, two tables EventInstance and Note. Several rows in EventInstance are related to one row in Note. Whenever user edits one EventInstance, changes to the linked Note are supposed to appear as well in all other EventInstances related to the same row.

This is part of my data model as of now:

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

    // ...

    modelBuilder.Entity<EventInstance>()
                .HasOne(i => i.Note)
                .WithMany()
                .HasForeignKey(i => i.NoteId);
}
    
public class EventInstance
{
    public int Id { get; set; }

    [Required]
    [Display(Name = "Spezifikation")]
    public required string Specification { get; set; }

    // ... 

    public int? NoteId { get; set; }
    [Display(Name = "Notiz")]
    public Note? Note { get; set; }

    // ...
}

public class Note
{
    public int Id { get; set; }
    [Display(Name = "Notiz"), DataType(DataType.MultilineText)]
    public string? Content { get; set; }
}

In my Create.cshtml.cs everything seem to work fine. I have manually coded these lines:

Note note = new Note() { Content = "" };

_context.Note.Add(note);
_context.SaveChanges();

int noteId = note.Id;

After creating a new EventInstance, I can check in the database that a new row in Event has been added and assigned an Id.

This is my EXISTING EditModel / PageModel für EventInstance:

public class EditModel : PageModel
{
    private readonly RazorPagesSchedule.Data.ScheduleDbContext _context;

    public EditModel(RazorPagesSchedule.Data.ScheduleDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public EventInstance EventInstance { get; set; } = default!;

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null || _context.EventInstance == null)
        {
            return NotFound();
        }

        var eventinstance = await _context.EventInstance
            .Include(i => i.EventTemplate)
            .Include(i => i.ResponsiblePerson)
            .Include(i => i.Note)
            .FirstOrDefaultAsync(m => m.Id == id);

        if (eventinstance == null)
        {
            return NotFound();
        }
        EventInstance = eventinstance;

        var list = new SelectList(_context.EventTemplate, "Id", "Description").ToList();
        list.Insert(0, new SelectListItem { Text = "--- Keine Aufgabenvorlage ---", Value = "" });
        ViewData["EventTemplateId"] = list;
        var persons = new SelectList(_context.Person.OrderBy(p => p.Sn), "Id", "Sn");
        ViewData["ResponsiblePersonId"] = persons;
        return Page();
    }

    // To protect from overposting attacks, enable the specific properties you want to bind to.
    // For more details, see https://aka.ms/RazorPagesCRUD.
    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(EventInstance).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!EventInstanceExists(EventInstance.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool EventInstanceExists(int id)
    {
      return (_context.EventInstance?.Any(e => e.Id == id)).GetValueOrDefault();
    }
}

EXPECTED BEHAVIOUR: When editing an Event, no new row in Note is to be created, but the existing linked row ought to be updated.


Solution

  • I feel like you should be loading the entity from the DB and modify it instead of taking it from the client verbatim, but the reason it’s creating a new Note is that you don’t tell it it’s modified. Add this:

    _context.Attach(EventInstance.Note).State = EntityState.Modified;
    

    To be safe, probably do a null check for Note first and be sure its Id is > 0. Really, you need to check that the Id actually exists, or you’ll get a foreign key exception.

    You also need the Note’s Id in your form:

    <input asp-for="@Model.EventInstance.Note.Id" hidden />
    

    Be aware it must be Note.Id not NoteId, as the latter would try to save a Note with Id 0, which would normally add a new row, but will crash when set to EntityState.Modified.