One of my entity is like below There are couple of related tables (continent, county, country ) where the foreign key is inserted in the database table
public class ContactAddress
{
[Key]
public int Id { get; set; }
[Required, MaxLength(150)]
public string AddressLine { get; set; }
[MaxLength(150)]
public string Town { get; set; }
[MaxLength(50)]
public string PostCode { get; set; }
[ForeignKey(nameof(Continent))]
public int? ContinentId { get; set; }
public Continent Continent { get; set; }
[ForeignKey(nameof(Country))]
public int? CountryId { get; set; }
public Country Country { get; set; }
[ForeignKey(nameof(County))]
public int? CountyId { get; set; }
public County County { get; set; }
}
This is the code for insert a new record to the database
public async Task<ContactAddress> AddNewContactAddress(AddressViewModel viewModel)
{
ContactAddress dbModel= new ContactAddress();
dbModel.AddressLine = viewmodel.AddressLine;
dbModel.Town=viewmodel.Town;
dbModel.PostCode=viewmodel.PostCode;
dbModel.ContinentId=viewmodel.ContinentId;
dbModel.CountryId=viewmodel.CountryId;
dbModel.CountyId=viewmodel.CountyId;
dbContext.ContactAddresses.Add(dbModel);
await context.SaveChangesAsync().ConfigureAwait(false);
return dbModel;
}
The returned dbModel contains all these informations plus the Id / auto incrememnt generated primary key for the inserted record But the associated informations of continent country county is missing on the retuned object
I created one more method
public async Task<ContactAddress> GetContactAddressDetails(int addressId)
{
return await context.ContactAddresses
.Include(c => c.Continent)
.Include(c => c.Country)
.Include(c => c.County)
.FirstOrDefaultAsync(p => p.Id == addressId);
}
public async Task<ContactAddress> AddNewContactAddress(AddressViewModel viewModel)
{
dbModel = convert fromm viewModel to dbModel
context.ContactAddresses.Add(dbModel);
await context.SaveChangesAsync().ConfigureAwait(false);
if (dbModel.Id > 0)
{
dbModel = await GetContactAddressDetails(dbModel.Id).ConfigureAwait(false);
}
return dbModel;
}
Is this is a correct approach of handling related records in entity framework? Can we avoid writing the extra method to get the related table informations after insert
CodePudding user response:
The first issue is that you want to effectively rely on lazy loading when you haven't constructed an EF Proxy.
Instead of this:
ContactAddress dbModel= new ContactAddress();
use:
ContactAddress dbModel = context.ContactAddresses.Create();
From there you can set your FK values, and EF should handle future requests to retrieve the navigation properties, lazy loading the data.
The approach I advocate for insert scenarios is to work with navigation properties rather than FK properties. Within entities I will generally declare one or the other, but not both. For "as fast as possible" operations I will just map FKs, for more complete views I will use navigation properties with shadow properties for the FKs. The primary reason for this is that if you have navigation properties and FK properties you now have two sources of truth for a relationship:
contactAddress.CountryId
// and
contactAddress.Country.CountryId
Some code that accepts a ContractAddress might use one, or the other, and you will get different behaviour depending on whether you set the FK or the navigation property, plus depending on whether the navigation property has been loaded or not. You also won't know if any provided value sent in is valid or not until you try calling SaveChanges(), leaving you to try and handle what to do if one or more FK values were invalid.
Working with navigation properties would look more like:
public async Task<ContactAddress> AddNewContactAddress(AddressViewModel viewModel)
{
ContactAddress dbModel= new ContactAddress();
dbModel.AddressLine = viewmodel.AddressLine;
dbModel.Town=viewmodel.Town;
dbModel.PostCode=viewmodel.PostCode;
dbModel.Continent = context.Continents.SingleOrDefault(x => x.ContinentId == viewModel.ContinentId) ?? throw new ArgumentException("The provided continent ID was not valid.");
dbModel.Country=context.Countries.SingleOrDefault(x => x.CountryId == viewModel.CountryId) ?? throw new ArgumentException("The provided country ID was not valid.");
dbModel.Country=context.Counties.SingleOrDefault(x => x.CountyId == viewModel.CountyId) ?? throw new ArgumentException("The provided county ID was not valid.");
dbContext.ContactAddresses.Add(dbModel);
await context.SaveChangesAsync().ConfigureAwait(false);
return dbModel;
}
The SingleOrDefault()
calls /w dedicated throws could just as easily be Single()
where the Expected 1, found 0
exception for the entity set in question would be raised. It just depends on whether you want to handle the exception to provide some feedback to the client or merely log the exception.
This avoids the need to reload the entities. It also asserts that each of the referenced IDs are themselves valid when creating the new row. You get back a complete model that is ready to go. Loading the entity might seem like an unnecessary cost, but given you wanted the data loaded after the call, it is no different a performance cost done here rather than a lazy load call when sent back. The benefit you get is the assertion that the values are valid with a meaningful "throw" point for each rather than once for all at the SaveChanges
call.