question

MrEdge-3552 avatar image
0 Votes"
MrEdge-3552 asked MrEdge-3552 commented

Return record ID before commiting changes?

I am using MVC 5 with Entity Framework 6 and Unit of Work pattern.

Tables: and fields

Customer - Id, Name

ContactType - Id, Name (Home contact, work contact etc)

ContactDetails - Id, CustomerId, ContactTypeId, ContactValue

I have created the Interfaces and Classes as required to carry out the basic Add, Edit functionality, then created an Unit Of Work class to hold all these Repositories.

Tested it out and everything works as expected when i hard code values in.

When i created my MVC application, i added the below lines to add this entry into a database using the Unit of Work class

     public ActionResult SaveContactDetails(CustomerContactType viewModel)
     {
         _unitOfWork.Customers.Add(viewModel.Customer);
         _unitOfWork.ContactDetails.Add(viewModel.ContactDetail);
         //_unitOfWork.SaveAllChanges();

         return View();
     }

I created a new ViewModel called CustomerContactType which is a class containing the tables i require in order to save the data successfully

     public class CustomerContactType
     {
         public ContactType ContactType { get; set; }
         public IEnumerable<ContactType> ContactTypes { get; set; }
         public Customer Customer { get; set; }
         public ContactDetail ContactDetail { get; set; }
     }

I realised how to assign a dropdown value to the model within the .cshtml, so the ContactDetails table knows which ContactType is associated with that contact number (Home, emergency etc).

The problem i have is the ContactDetails requires a customer ID. This customer ID doesnt generate until the customer is saved so im not sure how i should be doing this?

These two lines carry out the task but i can see the customerID is null in the second line where i would have preferred it to contain the ID

 _unitOfWork.Customers.Add(viewModel.Customer);
 _unitOfWork.ContactDetails.Add(viewModel.ContactDetail);

I can provide additional code if required but wasnt sure if theres an easy fix or not.

dotnet-aspnet-mvcdotnet-entity-framework
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

cooldadtx avatar image
0 Votes"
cooldadtx answered MrEdge-3552 commented

Firstly I notice you commented out the SaveChanges call in your action. If you don't save the changes then nothing will be created. The default behavior if the controller returns/fails before you save changes is to throw away all your changes.

EF is based upon proxies and relationships. You're thinking about this in terms of PK/FK relationships. Assuming you have set up the relationship in EF such that a Contact has an associated Customer and/or Customer has one or more Contact objects then ensure your navigation properties are marked as virtual. To create a new customer and contact you would simply create the customer object and add to your Customers set. Ensure the Customer object has a Contact assigned in its navigation property. When EF is told to add the customer it will see that there is also a Contact and, since the Contact is not being tracked yet, will automatically add the contact to the DB as well. You don't need to explicitly add it. Behind the scenes EF will (as part of the transaction) insert the Customer row and then insert the Contact row using the ID that was given when it inserted the Customer row. You don't have to do this manually.

More interesting is that once you call SaveChanges then EF, because of the proxy object for the virtual navigation property, will update the Id properties on your objects for you. So if you were to check the value of Customer.ID after the save call you'd see it is no longer invalid. The Contact.Id will be the same way. But unfortunately you'll still see a mixture of other behavior. For example the Customer property on Contact, if any, will probably not be set. EF caches data when it loads things and won't refresh them. So you should take a look at your objects after saving to see what EF actually set. I wouldn't be surprised if Contact.CustomerId is also not set as it isn't a virtual property. In general virtual nav properties will be updated by EF after save but other properties will not be.

· 8
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

I used this guide to set the project up https://www.programmingwithwolfgang.com/repository-and-unit-of-work-pattern/

I commented out the SaveChanges method when i was looking at the Object values in debug mode and what fields were being populated and not. I noted all the related properties (Navigational properties) for the ContactDetails tables were populated as long as i added a line such as below to the View (in MVC)


@Html.DropDownListFor(model => model.ContactDetail.ContactTypeID, new SelectList(Model.ContactTypes ,"Id","ContactTypeText"),"Please Select", new { htmlAttributes = new { @class = "form-control" } })

but i dont know what the equivalent is when adding a new object to pass in the ID from the Customer that was created into the ContactDetail table as its not yet generated.

Im considering what you recommended to change Add to virtual but in this context it wont make a difference as its void?

My other option is to change the Add to return a type i.e. T Add but i wanted to check there wasnt something i am missing before i changed the entire structure.

0 Votes 0 ·

I don't think I explained myself clearly. Add has nothing to do with this. The virtual is on the navigation properties that EF will set. Here's a quick example where I assume you have a 1:1 between Customer and Contact.

public class Customer
{
   public int Id { get; set; } //PK set by EF automatically

   //1:1
   public int ContactId { get; set; } //Used by EF to get to the contact
   public virtual Contact Contact { get; set; } //EF will handle setting this when retrieving data
 }

public class Contact
{
   public int Id { get; set; }

   public int CustomerId { get; set; }
   public virtual Customer Customer { get; set; } //EF will handle setting this when retrieving data
}


Let's add a customer and their contact.

var cust = new Customer() { Contact = new Contact() { }  };
_unitOfWork.Customers.Add(cust);
_unitOfWork.SaveAllChanges();

 var custId = cust.Id;  //EF automatically sets the Id of the new customer
var contactId = cust.Contact.Id;   //EF auto added contact to DB and set the ID as well


Let's get the customer

var cust = _unitOfWork.Customers.Find(10);  //customer with ID 10

var contactId = cust.ContactId;   //Retrieved from DB by EF
var contact = cust.Contact;  //EF will lazy load the associated contact, if any, provided you didn't disable lazy loading
var cust = cust.Contact.Customer;  //EF already loaded customer so you get the same value as `cust`
0 Votes 0 ·

I'm assuming you have properly defined your DB relationships either via attributes or an entity type configuration.

0 Votes 0 ·

I think i know what you mean. This is what i have in my Customer class - i reversed engineered it but it should be the same

public partial class Customer
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Customer()
{
ContactDetails = new HashSet<ContactDetail>();
}

     public int Id { get; set; }

     [Required]
     [StringLength(50)]
     public string Name { get; set; }

     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
     public virtual ICollection<ContactDetail> ContactDetails { get; set; }
 }

I see what you are doing in your example. In short i was attempting to see if i could save the Customer and contacts in one transaction.... So i save a customer successfully and something happens when creating a contact number so it doesnt save..... I now have a customer name in the customer table but no contact associated with it. Perhaps i should have made this a little clearer before :-) but thanks for your help

0 Votes 0 ·
Show more comments
cooldadtx avatar image
0 Votes"
cooldadtx answered YijingSun-MSFT commented

(Responding to your question with an answer so I have more space)

Firstly I'll just say that using your EF data objects as your models for MVC is going to cause you problems down the road. Besides the fact that your exposing your DB structure to a view which drastically limits your options it also means that your MVC layer is going to have to be managing EF state which is tricky. But I'll let you worry about that later.

Are you sure about your ContactType structure? It has a nav property to ContactDetail but that means you have a need to fetch all contacts (across all customers) that have a particular type. This seems like a perf issue just waiting to happen. I don't see ContactType being anything more than a lookup table so it wouldn't have nav properties.

As for the double save issue it is because of how you're building your view model. In your view model you separate out the customer from the contact (of which your model allows one). When you are trying to add EF isn't looking at your model's single contact (because it knows nothing about your viewmodel). It is looking at the customer's ContactDetails property. So before adding the customer do this.

var customer = viewModel.Customer;
customer.ContactDetails.Add(viewModel.ContactDetail);
customer = _unitOfWork.Customers.Add(customer);
_unitOfWork.SaveAllChanges();


By adding to the customer you are letting EF know about the relationship that was implied in the VM. Additionally notice that I'm capturing the result of calling Add. This is important because that is the proxy that EF will update on save. You need to use it to get the added data after save. For example it will have the IDs from the DB and the ContactDetails will contain the updated info as well.

· 4
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Thanks I will have a look at that..... The way I built the application and separated the view model was based on the MVC course I went through/going through :-(

0 Votes 0 ·

Hi @MrEdge-3552 ,

The way I built the application and separated the view model was based on the MVC course I went through/going through :-(

When we talk about Views and Controllers, their ownership itself explains separation.The Views are just the presentation form of an application, it does not need to know specifically about the requests coming from the Controller. The Model is independent of Views and Controllers, it only holds business entities that can be ed to any View by the Controller as per the needs for exposing them to the end user. The Controller is independent of Views and Models, its sole purpose is to handle requests and them on as per the routes defined and as per the needs of the rendering Views. Thus our business entities (Model), business logic (Controllers) and presentation logic (Views) lie in logical/physical layers independent of each other. I think your current problem is connect the customers with the contactDetail.
Best regards,
Yijing Sun

0 Votes 0 ·

Is there a link I could refer to understand what the correct way I should be doing this?

If the tutorial is wrong I would like to point this out to the author.

0 Votes 0 ·
Show more comments