1

I am struggling to setup a scenario (using C# classes and OnModelCreating() method) where DB models would act as follows:

First assumption, the mandatory one:

  1. I want to have the ability to create User (AspNetUsers table) without a reference to a Guest. It will be necessary while seeding a DB with an admin user - it will not belong to any of the Event-s. In summary - at least that's my understanding - User will be PRINCIPAL, Guest will be DEPENDENT (?)

  2. Cascading deletion: I want to delete Users from AspNetUsers table when I delete a given Event (cascade delete). This functionality already exists for Guests. When I delete an Event, all related Guests are being deleted correctly.

Two questions:

1. How do I actually create Guests that are related to AspNetUsers table?

When it comes to Guests list and its assignement to a ceratin Event, I just do something like:

    eventDbObject.Guests = GetGuestsList();
    
    _dbContext.Events.Add(evenDbObject); //Event is created in Events table, Guests table is correctly populated as well

With users it's tricky - I have to Register them first, get their ID, and then assign that ID to a Guest object. Is that way correct?

foreach (var guest in weddingDbObject.Guests)
{
    var userCreationResult = await _identityService.RegisterAsync("userName","password"); // my RegisterAsync() method returns actual User

    guest.AppUser = userCreationResult.User;
}

2. How to set up cascade deletion in such a scenario?

builder
    .Entity<Guest>()
    .HasOne(e => e.AppUser)
    .WithOne(e => e.Guest)
    .OnDelete(DeleteBehavior.Cascade);

Something like this does not seem to work

My classes:

public class Event
{
    // PK
    public Guid Id {get;set;}
    
    public List<Guest> Guests { get; set; }
}

public class Guest
{
    // PK
    public Guid Id { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    
    public Guid AppUserId { get; set; }
    public AppUser AppUser { get; set; }
}

public class AppUser : IdentityUser<Guid>
{
    public WeddingGuest Guest { get; set; }
}
Kamil Turowski
  • 427
  • 1
  • 4
  • 13
  • The principal cannot be cascade deleted from dependent. Cascade delete works other way around. Hence your 2 requirements are mutually exclusive. – Ivan Stoev Feb 21 '22 at 17:59
  • So I would have to make Guest principal? How about seeding the data then? I don't want to end up creating "admin guest", just to have something to assign "admin user" to... – Kamil Turowski Feb 21 '22 at 18:10
  • You can eventually make it optional. i.e. `User` with *nullable* FK to `Guest`. It will allow you to create `User` w/o `Guest`. At the same time cascade delete can be turned on (it by default is off for optional relationships). Pretty much like in one-to-many. – Ivan Stoev Feb 21 '22 at 18:24
  • @IvanStoev - could You take a look at the answer I posted? Anything that I could do differently? I ended up with Guid? as `.IsRequired(false)` was not enough – Kamil Turowski Feb 22 '22 at 09:49
  • `Guid?` (`Nullable`) is what I meant by *nullable* FK in my previous comment. All good. – Ivan Stoev Feb 22 '22 at 10:53

1 Answers1

1

Ok, this is what I ended up with, assumptions are met, everything works as expected.

public async Task<string> AddEventAsync(Event event)
{
    using (var transaction = _dbContext.Database.BeginTransaction())
    {
        try
        {
            // Add an event to [Events] table and corresponding guests to [Guests] table
            _dbContext.Events.Add(event);

            // Add users to [AspNetUsers] table
            foreach (var guest in event.Guests)
            {
                var userCreationResult = await _identityService.RegisterAsync(guest.Id, $"{event.Name}-{guest.FirstName}-{guest.LastName}", guest.GeneratedPassword);

                if (!userCreationResult.Success)
                    throw new Exception();

                guest.AppUser = userCreationResult.User;
            }

            transaction.Commit();
            _dbContext.SaveChanges();
        }
        catch
        {
            transaction.Rollback();
        }
    }

    return event.Name;
}

Classes look like this:

public class Event
{
    // PK
    public Guid Id {get;set;}
    
    public List<Guest> Guests { get; set; }
}

// PRINCIPAL
public class Guest
{
    // PK
    public Guid Id { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    
    [JsonIgnore]
    public AppUser AppUser { get; set; }
}

// DEPENDENT
public class AppUser : IdentityUser<Guid>
{
    public Guid? GuestId { get; set; } // '?' allows for 1:0 relationship

    [JsonIgnore]
    public Guest Guest { get; set; }
}

Fluent API DbContext configuration:

builder.Entity<AppUser>(entity =>
{
    entity.HasOne(wg => wg.Guest)
         .WithOne(a => a.AppUser)
         .HasForeignKey<AppUser>(a => a.GuestId)
         .IsRequired(false)
         .OnDelete(DeleteBehavior.Cascade);
});
Kamil Turowski
  • 427
  • 1
  • 4
  • 13