3

I have an ASP.NET MVC website that is passing ViewModels up to a WebAPI Service in order to perform CRUD operations on objects in the database using Entity Framework code first.

One of my Domain Entity Objects is a Business, which has business information, an address, a primary contact, and secondary contact. The thing is, the secondary contact object is not required since not all companies will have a secondary contact. I have setup the ID field as nullable, but when I attempt to save a new business record into the database that doesn't have a secondary contact object, it is giving me entity validation errors that the Secondary Contact record fields are required.

Does anyone know how to stop this error so I can save the business entity in the database without a secondary contact?

Below is the related code. I am using automapper to map between my viewmodels and models.

Domain Business Object

    public class Business
{
    public Guid PrimaryContactId { get; set; }
    [ForeignKey("PrimaryContactId")]
    [Required]
    public virtual Contact PrimaryContact { get; set; }

    public Guid? SecondaryContactId { get; set; }
    [ForeignKey("SecondaryContactId")]
    public virtual Contact SecondaryContact { get; set; }

    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string CompanyName { get; set; }

    [MaxLength(100)]
    public string CompanyWebsite { get; set; }

    [Required]
    [MaxLength(100)]
    public string Industry { get; set; }

    [Required]
    public NumberOfEmployees NumberOfEmployees { get; set; }

    [Required]
    public CommunicationPreference CommunicationPreference { get; set; }

    public Guid AddressId { get; set; }

    [ForeignKey("AddressId")]
    [Required]
    public virtual Address Address { get; set; }
}

Business View Model

public class BusinessViewModel
{
    public Guid PrimaryContactId { get; set; }
    public PrimaryContactViewModel PrimaryContact { get; set; }

    public Guid? SecondaryContactId { get; set; }
    public SecondaryContactViewModel SecondaryContact { get; set; }

    public Guid Id { get; set; }

    [DisplayName("Company Name")]
    [Required]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public string CompanyName { get; set; }

    [DisplayName("Company Website")]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public string CompanyWebsite { get; set; }

    [DisplayName("Industry")]
    [Required]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public string Industry { get; set; }

    [DisplayName("Number of Employees")]
    [Required]
    public NumberOfEmployees NumberOfEmployees { get; set; }

    [DisplayName("Communication Preference")]
    [Required]
    public CommunicationPreference CommunicationPreference { get; set; }

    public Guid AddressId { get; set; }
    [Required]
    public AddressViewModel Address { get; set; }
}

Domain Contact Object

    public class Contact
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    public Salutations? Prefix { get; set; }

    [Required]
    [MaxLength(100)]
    public string FirstName { get; set; }

    [Required]
    [MaxLength(100)]
    public string LastName { get; set; }

    [Required]
    [MaxLength(100)]
    public string JobTitle { get; set; }

    [Required]
    [DataType(DataType.EmailAddress)]
    [MaxLength(100)]
    public string Email { get; set; }

    [Required]
    [MaxLength(100)]
    public string Phone { get; set; }

    [MaxLength(100)]
    public string PhoneExtension { get; set; }
}

Base Contact View Model

    public class BaseContactViewModel
{
    public Guid Id { get; set; }

    [DisplayName("Primary Contact Prefix")]
    public virtual Salutations Prefix { get; set; }

    [DisplayName("Primary Contact First Name")]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string FirstName { get; set; }

    [DisplayName("Primary Contact Last Name")]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string LastName { get; set; }

    [DisplayName("Primary Contact Job Title")]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string JobTitle { get; set; }

    [DisplayName("Primary Contact Email Address")]
    [DataType(DataType.EmailAddress)]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string Email { get; set; }

    [DisplayName("Primary Contact Phone")]
    [DataType(DataType.PhoneNumber)]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string Phone { get; set; }

    [DisplayName("Primary Contact Extension")]
    [MinLength(3, ErrorMessage = "Your {0} must be at least {1} characters long")]
    [MaxLength(100, ErrorMessage = "Your {0} must be no more than {1} characters")]
    public virtual string PhoneExtension { get; set; }
}

Secondary Contact View Model

public class SecondaryContactViewModel : BaseContactViewModel
{

    [DisplayName("Secondary Contact Prefix")]
    public override Salutations Prefix { get; set; }

    [DisplayName("Secondary Contact First Name")]
    public override string FirstName { get; set; }

    [DisplayName("Secondary Contact Last Name")]
    public override string LastName { get; set; }

    [DisplayName("Secondary Contact Job Title")]
    public override string JobTitle { get; set; }

    [DisplayName("Secondary Contact Email Address")]
    public override string Email { get; set; }

    [DisplayName("Secondary Contact Phone")]
    public override string Phone { get; set; }

    [DisplayName("Secondary Contact Extension")]
    public override string PhoneExtension { get; set; }

}

Method that does the EF saving

public async Task<bool> CreateBusinessAsync(BusinessViewModel businessViewModel)
    {
        try
        {
            // TODO: Move mapping to some common place?
            Mapper.CreateMap<BusinessViewModel, Business>();
            Mapper.CreateMap<BaseContactViewModel, Contact>();
            Mapper.CreateMap<AddressViewModel, Address>();


            Business business = Mapper.Map<Business>(businessViewModel);

            //TODO: See why EntityFramework isn't automatically putting in GUIDs
            business.Id = Guid.NewGuid();
            business.Address.Id = Guid.NewGuid();
            business.PrimaryContact.Id = Guid.NewGuid();


            // Attach the objects so they aren't saved as new entries
            db.States.Attach(business.Address.State);
            db.Countries.Attach(business.Address.Country);

            //TODO: See why entity framework isn't automatically saving records
            var bus = db.Businesses.Add(business);
            var primary = db.Contacts.Add(business.PrimaryContact);

            if (!String.IsNullOrEmpty(business.SecondaryContact.FirstName))
            {
                business.SecondaryContact.Id = Guid.NewGuid();
                db.Contacts.Add(business.SecondaryContact);
            }
            else
            {
                business.SecondaryContact = null;
            }


            var address = db.Addresses.Add(business.Address);

            int rowsAffected = await db.SaveChangesAsync();


            if (bus != null && rowsAffected > 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        catch (Exception e)
        {
            //TODO: Add exception logger
            string error = e.Message;

            return false;
        }

    }

Please let me know if it would be useful to see any other classes. Thanks.

reverence12389
  • 457
  • 5
  • 15
  • This line !String.IsNullOrEmpty(business.SecondaryContact.FirstName) makes me believe that you are passing in a secondary contact but it is missing some required information. If SecondaryContact was null that would throw an exception. – Andres Castro Aug 03 '15 at 17:08
  • Yeah, I added that as part of some test code to see if maybe SecondaryContact was coming back as a non-complete object, so I was attempting to set it to null so it wouldn't try and add the entity to the database. It still gives the same error. The code is failing on the savechanges, even if SecondaryContact is set to null, it is still validating saying that the fields are required. – reverence12389 Aug 03 '15 at 17:21
  • Can you check your database and make sure that column allows null? If not then maybe you didn't do a database migration and update. – Andres Castro Aug 03 '15 at 17:32
  • Yes @andres, the SecondaryContactId column on the dbo.Business table allows nulls. See table design here: [link](http://imgur.com/mK4vPQT) – reverence12389 Aug 03 '15 at 17:37
  • you try not using automapper to build your Business entity? – JamieD77 Aug 03 '15 at 17:56
  • 1
    On a side note, I would avoid using GUID as a key. It is 128 bits wide, as opposed to 32 bits for an integer key. While disk storage is cheap, the DB can only cache 1/4th as many GUID keys due to the larger bit size. The moment your working set exceeds your RAM, the database will begin accessing the disk more heavily. – Eric J. Aug 03 '15 at 20:58
  • @EricJ. I thought it was standard to use GUIDs as a primary key in a relational DB like SQL; isn't there the potential to run out of unique keys if you just use int? – reverence12389 Aug 04 '15 at 19:55
  • You should allow the database to assign the actual INT value for the key (all modern databases have a way to do that). That way, you will not run into duplicates. – Eric J. Aug 05 '15 at 16:04
  • @EricJ When I didn't assign the Guid.NewGuid() in .NET code before calling the EntityFramework SaveChanges method, it wasn't giving them a unique Guid in the database, it was assigning the default 00000000-0000.... – reverence12389 Aug 05 '15 at 16:39
  • I followed the answer here [link](http://stackoverflow.com/questions/23081096/entity-framework-6-guid-as-primary-key) to resolve the issue of the GUIDs not being generated by EF – reverence12389 Aug 05 '15 at 16:56

2 Answers2

0

I had a similar issue before and the way I fixed it was by taking the foreign key data annotation off of the property and using fluent-api. Using fluent-api I just labeled the correct side of the relationship as optional.

tcrite
  • 523
  • 2
  • 12
  • this worked, I just removed the ForeignKey DataAnnotation and instead used fluent api to build the foreign key relationship like so. modelBuilder.Entity().HasOptional(t => t.SecondaryContact).WithMany().HasForeignKey(x => x.SecondaryContactId); However, another issue I'm having is that the SecondaryContactViewModel is never coming back as null from the view, so Entity Framework is always trying to save it. Any idea how to resolve that issue? – reverence12389 Aug 04 '15 at 19:46
  • @reverence12389 Is the SecondaryContactId coming back null? – tcrite Aug 04 '15 at 20:27
  • Yes, the secondarycontactid is coming back null and all the properties of the secondarycontact are coming back null, but secondarycontact is still a SecondaryContactViewModel object. – reverence12389 Aug 04 '15 at 20:34
  • @reverence12389 This may be tedious, and there may be a better way, but what I've done in this situation is check whether the Id is null, and if so, I set the object to null before persisting. However, like I said there may be a better and quicker way to handle it. – tcrite Aug 04 '15 at 20:39
0

You can just make the data type to nullable based on documentation

public class Foo {
// ...... Some property
[ForeignKey("tagId")]
public ICollection<SomeModel>? data {get; set;}
}

This will make the property not validated and return validated result. Using Model.IsValid

Benyamin Limanto
  • 775
  • 10
  • 24