I am learning C# and I have been running into a lot of issues with my first project around navigation properties. The main hurdle I am having an issue with is a basic four property model (and anything that includes a navigation property).
Here is the one I am currently working with:
Company Model:
public class Company
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Display(Name = "Id")]
public int Id { get; set; }
[Display(Name = "Name")]
public string Name { get; set; }
public CompanyType Type { get; set; }
public ICollection<Contact>? Contacts { get; set; }
}
Company Type Model:
public class CompanyType
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Display(Name = "Id")]
public int Id { get; set; }
[Display(Name = "Company Type")]
public string Type { get; set; } = "";
}
And here is my context - just simple.
public class SRMContext : DbContext
{
public DbSet<Company> Companies { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<CompanyType> CompanyTypes { get; set; }
public SRMContext (DbContextOptions<SRMContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Company>().ToTable("Companies");
modelBuilder.Entity<Contact>().ToTable("Contacts");
modelBuilder.Entity<CompanyType>().ToTable("CompanyTypes");
}
}
*I removed irrelevant models in the context.
I first used scaffolding to create the CRUD razor pages for Company. It would not include any navigation properties in it at all - for any model. Not sure if I am doing something wrong, or if this is standard. So I am trying to manually update the scaffolded pages to include CompanyType. I was able to generate a select list, and provide that to the UI. It gives me the options and I click submit. But when I click submit, it says I need to include the Company Type. If I change it to not required, it will submit, but there will be no company type listed.
I know you can just do the Id without doing the navigation property, but further in my code, I have many-to-many navigation properties, so the Id will not work for them. I am having the same issues with them too, but this one is much simpler to work with until I get this figured out.
What am I doing wrong?
public class CreateModel : PageModel
{
private readonly SRMContext _context;
public SelectList CompanyTypes { get; set; }
public CreateModel(SRMContext context)
{
_context = context;
}
public async Task<IActionResult> OnGetAsync()
{
CompanyTypes = new SelectList(_context.CompanyTypes, nameof(CompanyType.Id), nameof(CompanyType.Type));
return Page();
}
[BindProperty]
public Company Company { get; set; }
// To protect from overposting attacks, see https://aka.ms/RazorPagesCRUD
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Companies.Add(Company);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
View Page
<div >
<div >
<form method="post">
<div asp-validation-summary="ModelOnly" ></div>
<div >
<label asp-for="Company.Name" ></label>
<input asp-for="Company.Name" />
<span asp-validation-for="Company.Name" ></span>
</div>
<div >
<label asp-for="Company.Type" ></label>
<select asp-for="Company.Type" asp-items="Model.CompanyTypes" ></select>
<span asp-validation-for="Company.Type" ></span>
</div>
<div >
<input type="submit" value="Create" />
</div>
</form>
</div>
</div>
(I'm sorry, I know its a lot of code, but I want to show everything needed!)
CodePudding user response:
Complex objects are not recognized on a form post, so the type is probably null. Instead, post the foreign key of the type and it should be recognized:
public class Company
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
[Display(Name = "Id")]
public int Id { get; set; }
[Display(Name = "Name")]
public string Name { get; set; }
public int CompanyType_Id { get; set; }
[ForeignKey("CompanyType_Id")]
public CompanyType Type { get; set; }
public ICollection<Contact>? Contacts { get; set; }
}
And in your form:
<select asp-for="Company.CompanyType_Id" asp-items="Model.CompanyTypes" ></select>
Haven't tested this, but pretty sure this is the issue.
CodePudding user response:
See my answer to this question (Binding Complex Entities inside complex Entities to Requests) for an explanation of what is happening when you send an entity to a View to serve as it's Model and the issues & risks you can encounter when trying to send that model back.
The general advice I give is to not send entities to the view because while the view engine on the server gets the entity, what comes back from the client browser is not still an entity, it is just data fields that get cast as an entity so they are either incomplete or prone to tampering. (plus generally involve sending far more data over the wire than is needed)
Your Create PageModel is a good start, but consider just embedding the fields from a Customer rather than a Customer entity (Same goes for Update/Edit) This would include a CustomerTypeId column that when you build your Customer entity within the POST call, you fetch the CustomerType reference from the DbContext and assign that to the newly created Customer. This has the benefit of also validating whatever FKs you receive from the client browser to ensure you are dealing with legal data, throwing an exception in a more useful spot for debugging than the final SaveChanges
call. The typical rationale for wanting to pass entities around is to avoid additional DB Read hits. Reading related entities like this by ID is extremely fast, and even cases where you might need to load several related entities there are techniques to batch these up into a single read operation. (Select all IDs needed, read in a single hit using a Contains
, then assign from that set)