Issue with Setting Relationships in Business Entity when Creating New Business with Associated Services and Workers

24 views Asked by At

When attempting to create a new Business entity by passing a JSON payload that includes associated Services and Workers, I noticed that the Services and Workers collections within the Business entity are not correctly establishing the relationships. Upon inspection in the debugger, both the Services and Workers collections had their Business and Workers properties set to null, indicating a problem with the relationship mapping.

Business Model

using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Business : IdentityUser
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [JsonIgnore]
    public int BusinessID { get; set; }

    [Required(ErrorMessage = "Username is required")]
    public string BusinessUsername { get; set; }

    [Required]
    public string BusinessName { get; set; }

    [Required]
    public string BusinessPhone { get; set; }

    [Required]
    public string BusinessEmail { get; set; }

    [Required]
    public string BusinessLocation { get; set; }

    [Required]
    public string BusinessDescription { get; set; }

    [Required]
    public string BusinessCategory { get; set; }

    [Required(ErrorMessage = "First name is required")]
    public string OwnerFirstName { get; set; }

    [Required(ErrorMessage = "Last name is required")]
    public string OwnerLastName { get; set; }

    [Required(ErrorMessage = "Password is required")]
    [MinLength(6, ErrorMessage = "Password must be at least 6 characters long")]
    public string BusinessPassword { get; set; }

    [Required]
    public DateTime BusinessEstablishedDate { get; set; }

    // Navigation property to represent the services offered by the business
    [JsonIgnore]
    public ICollection<Service> Services { get; set; }

    // Navigation property to represent the workers employed by the business
    [JsonIgnore]
    public ICollection<Worker> Workers { get; set; }
}

Service Model

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

public class Service
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [JsonIgnore]
    public int ServiceID { get; set; }

    [Required]
    public string ServiceName { get; set; }

    public string Description { get; set; }

    [Required]
    public int Duration { get; set; }

    [Required]
    public decimal Price { get; set; }


    [Required]
    [JsonIgnore]
    public int BusinessId { get; set; } // Foreign key to associate service with a business

    // Navigation property to represent the relationship with business
    [JsonIgnore]
    public Business Business { get; set; }

    // Navigation property to represent the workers providing this service
    [JsonIgnore]
    public ICollection<Worker> Workers { get; set; }
}

Worker Model

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;

public class Worker
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [JsonIgnore]
    public int WorkerID { get; set; }

    [Required]
    public string WorkerFirstName { get; set; }

    [Required]
    public string WorkerLastName { get; set; }

    [Required]
    [Phone]
    public string WorkerPhone { get; set; }

    [Required]
    [EmailAddress]
    public string WorkerEmail { get; set; }

    [Required]
    [JsonIgnore]
    public int BusinessId { get; set; } // Foreign key to associate worker with a business

    // Navigation property to represent the relationship with business
    [JsonIgnore]
    public Business Business { get; set; }

    // Navigation property to represent the services the worker can provide
    [JsonIgnore]
    public ICollection<Service> Services { get; set; }
}

Business Service

using AppointSmart.Data;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class BusinessService : IBusiness
{
    private readonly ApplicationDbContext _context;

    public BusinessService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Business> GetBusinessByIdAsync(int businessId)
    {
        return await _context.Businesses.FindAsync(businessId);
    }

    public async Task<Business> RegisterBusinessAsync(Business businessModel)
    {
        // Validate input (e.g., check for required fields, password strength, etc.)

        // Hash the user's password

        // Create a new User object
        var newUser = new Business
        {
            BusinessUsername = businessModel.BusinessUsername,
            BusinessName = businessModel.BusinessName,
            BusinessPhone = businessModel.BusinessPhone,
            BusinessEmail = businessModel.BusinessEmail,
            BusinessLocation = businessModel.BusinessLocation,
            BusinessDescription = businessModel.BusinessDescription,
            BusinessCategory = businessModel.BusinessCategory,
            OwnerFirstName = businessModel.OwnerFirstName,
            OwnerLastName = businessModel.OwnerLastName,
            BusinessPassword = businessModel.BusinessPassword,
            BusinessEstablishedDate = businessModel.BusinessEstablishedDate
        };

        // Add the user to the database
        _context.Businesses.Add(newUser);
        try
        {
            await _context.SaveChangesAsync();
            return newUser;
        }
        catch (DbUpdateException ex)
        {
            // Handle database exception
            throw new ApplicationException("Error occurred while registering the business.", ex);
        }
    }

    public async Task<Business> GetBusinessByUsernameAsync(string businessUsername)
    {
        return await _context.Businesses.FirstOrDefaultAsync(b => b.BusinessUsername == businessUsername);
    }

    public async Task<Business> GetBusinessByEmailAsync(string businessEmail)
    {
        return await _context.Businesses.FirstOrDefaultAsync(b => b.BusinessEmail == businessEmail);
    }

    public async Task<Business> UpdateBusinessAsync(int businessId, Business business)
    {
        var existingBusiness = await _context.Businesses.FindAsync(businessId);

        if (existingBusiness == null)
        {
            throw new ApplicationException("Business not found");
        }

        existingBusiness.BusinessName = business.BusinessName;
        existingBusiness.BusinessPhone = business.BusinessPhone;
        existingBusiness.BusinessEmail = business.BusinessEmail;
        existingBusiness.BusinessLocation = business.BusinessLocation;
        existingBusiness.BusinessDescription = business.BusinessDescription;
        existingBusiness.BusinessCategory = business.BusinessCategory;
        existingBusiness.OwnerFirstName = business.OwnerFirstName;
        existingBusiness.OwnerLastName = business.OwnerLastName;

        await _context.SaveChangesAsync();

        return existingBusiness;
    }

    public async Task<bool> DeleteBusinessAsync(int businessId)
    {
        var existingBusiness = await _context.Businesses.FindAsync(businessId);

        if (existingBusiness == null)
        {
            throw new ApplicationException("Business not found");
        }

        _context.Businesses.Remove(existingBusiness);
        await _context.SaveChangesAsync();

        return true;
    }
}

Business Controller

using AppointSmart.Data;
using AppointSmart.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.Web.Http.ModelBinding;

namespace AppointSmart.Controllers
{
    [ApiController]
    public class BusinessRegistrationController : ControllerBase
    {
        private readonly ApplicationDbContext _context;
        private readonly IBusiness _businessService;

        public BusinessRegistrationController(ApplicationDbContext context, IBusiness businessService)
        {
            _businessService = businessService;
            _context = context;
        }

        // Endpoint for user registration
        [HttpPost("businessRegister")]
        public async Task<IActionResult> BusinessRegisterAsync([FromBody] Business businessModel)
        {
            try
            {
                // Attempt to register the user
                var registeredBusiness = await _businessService.RegisterBusinessAsync(businessModel);

                // Return the newly created user (you can also return a success message)
                return CreatedAtAction(nameof(GetBusinessByIdAsync), new { userId = registeredBusiness.Id }, registeredBusiness);
            }
            catch (Exception ex)
            {
                // Handle exceptions and return an appropriate error response
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }

        [HttpPut]
        [Route("UpdateBusinessAsync/{businessId}")]
        public async Task<IActionResult> UpdateBusinessAsync(int businessId, Business business)
        {
            try
            {
                var result = await _businessService.UpdateBusinessAsync(businessId, business);
                return Ok(result);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"An error occurred: {ex.Message}");
            }
        }

        [HttpDelete]
        [Route("DeleteBusinessAsync/{businessId}")]
        public async Task<IActionResult> DeleteBusinessAsync(int businessId)
        {
            try
            {
                var result = await _businessService.DeleteBusinessAsync(businessId);
                return Ok(result);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"An error occurred: {ex.Message}");
            }
        }

        // Endpoint for user login
        [HttpGet("GetBusinessByIdAsync/{businessId}")]
        public async Task<IActionResult> GetBusinessByIdAsync(int businessId)
        {
            try
            {
                var user = await _businessService.GetBusinessByIdAsync(businessId);
                if (user == null)
                {
                    return NotFound();
                }
                return Ok(user);
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"Internal server error: {ex.Message}");
            }
        }

        // Endpoint for getting user by username
        [HttpGet("GetBusinessByUsernameAsync/{businessUsername}")]
        public async Task<IActionResult> GetBusinessByUsernameAsync(string username)
        {
            var user = await _businessService.GetBusinessByUsernameAsync(username);
            if (user == null)
            {
                return NotFound();
            }

            return Ok(user);
        }

        // Endpoint for getting user by email
        [HttpGet("GetBusinessByEmailAsync/{businessEmail}")]
        public async Task<IActionResult> GetBusinessByEmailAsync(string email)
        {
            var user = await _businessService.GetBusinessByEmailAsync(email);
            if (user == null)
            {
                return NotFound();
            }

            return Ok(user);
        }
    }
}

When I remove the [JsonIgnore] and pass the below payload, I receive an error

Payload

{
  "BusinessUsername": "new_business",
  "BusinessName": "New Business Name",
  "BusinessPhone": "987-654-3210",
  "BusinessEmail": "[email protected]",
  "BusinessLocation": "456 Elm Street",
  "BusinessDescription": "Description of the new business",
  "BusinessCategory": "New Category",
  "OwnerFirstName": "Jane",
  "OwnerLastName": "Smith",
  "BusinessPassword": "securepassword123",
  "BusinessEstablishedDate": "2022-02-15",
  "Services": [
    {
      "ServiceName": "Service A",
      "Description": "Description of Service A",
      "Duration": 60,
      "Price": 50.00
    },
    {
      "ServiceName": "Service B",
      "Description": "Description of Service B",
      "Duration": 30,
      "Price": 35.00
    }
  ],
  "Workers": [
    {
      "WorkerFirstName": "John",
      "WorkerLastName": "Doe",
      "WorkerPhone": "555-123-4567",
      "WorkerEmail": "[email protected]"
    },
    {
      "WorkerFirstName": "Alice",
      "WorkerLastName": "Smith",
      "WorkerPhone": "555-987-6543",
      "WorkerEmail": "[email protected]"
    }
  ]
}

*** Error ***

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-243f2a7070d552e248f5ea551282134b-4fa947568f710590-00", "errors": { "Workers[0].Business": [ "The Business field is required." ], "Workers[0].Services": [ "The Services field is required." ], "Workers[1].Business": [ "The Business field is required." ], "Workers[1].Services": [ "The Services field is required." ], "Services[0].Workers": [ "The Workers field is required." ], "Services[0].Business": [ "The Business field is required." ], "Services[1].Workers": [ "The Workers field is required." ], "Services[1].Business": [ "The Business field is required." ] } }
1

There are 1 answers

2
wertzui On

The problem here are your [JsonIgnore] attributes on your relationships. When JSON is coming into your controller, these attributes tell the model binder to explicitly ignore these properties even if they have any value in the JSON.

If you remove the attribute, you will get the JSON deserialized into objects and those will be persisted into your database.

However it is bad design to use the same models in your database and your API. Better would be to create DTOs to pass in and out of your API and then use something like AutoMapper to map between them. Best would be to create separate DTOs for your separate controller actions. You would then have one DTO for the create operation which would include the relationships and another one for update which does not include the relationships.