tokyo0709
tokyo0709

Reputation: 1997

Web Api Updating only specific properties through a binding model

We are currently building a web api and controllers for each of our data tables with basic CRUD functionality. The issue we are running into is with Updates. We've created custom binding models to bring in only the data we need, then convert that binding model to an object, and pass it to our update function.

The problem we are running into is that when the client sends data through a POST, our binding model recieves it and populates the fields they set with the values, and everything else it populates as null. So when we convert it to the data object and send it to the Update function it overrides fields that weren't set from the client to null.

This is obviously going to cause issues as we don't want users to be accidentally deleting information.

Here is an example of how we are running things with the client, binding model, and updates,

The Team Binding Model

/// <summary>A Binding Model representing the essential elements of the Team table</summary>
public class TeamBindingModel
{
    /// <summary>The Id of the team</summary>
    [Required(ErrorMessage = "An ID is required")]
    public int ID { get; set; }

    /// <summary>The name of the team</summary>
    [Required(ErrorMessage = "A Team Name is required")]
    [Display(Name = "Team Name")]
    [StringLength(35)]
    public string Team1 { get; set; }

    /// <summary>The email associated with the team</summary>
    [StringLength(120)]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    public bool ShowDDL { get; set; }
}

The UpdateTeam CRUD Method

// PUT: api/Team
/// <summary>
/// Attempt to update a team with a given existing ID
/// </summary>
/// <param name="team">TeamBindingModel - The binding model which needs an Id and a Team name</param>
/// <returns>IHttpActionResult that formats as an HttpResponseCode string</returns>
[HttpPut]
[Authorize(Roles = "SystemAdmin.Teams.Update")]
public async Task<IHttpActionResult> UpdateTeam(TeamBindingModel team)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    try
    {
        // Convert the binding model to the Data object
        Team teamObject = team.ToObject();

        unitOfWork.TeamRepository.Update(teamObject);
        await unitOfWork.Save();
    }
    catch (DbUpdateConcurrencyException)
    {
        return BadRequest();
    }
    catch (Exception ex)
    {
        return BadRequest(ex.Message);
    }

    return Ok();
}

The ToObject Function

/// <summary>Takes the Team Binding model and converts it to a Team object</summary>
/// <returns>Team Object</returns>
public virtual Team ToObject()
{
    // Setup the data object
    Team newObject = new Team();

    // Instantiate the basic property fields
    newObject.ID = this.ID;
    newObject.Team1 = this.Team1;
    newObject.Email = this.Email;
    newObject.ShowDDL = this.ShowDDL;

    return newObject;
}

The Update Function

public virtual void Update(TEntity entityToUpdate)
{
    try
    {
        dbSet.Attach(entityToUpdate);
        dbContext.Entry(entityToUpdate).State = EntityState.Modified;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

The Save Function

public async Task Save()
{
    await dbContext.SaveChangesAsync();
}

Client calls / Testing / Error

// Add team to update and remove
var db = new genericDatabase();
var teamDB = new Team { Team1 = "testTeam", Email = "[email protected]", ShowDDL = true};

db.Teams.Add(teamDB);
db.SaveChanges();

// Look for items in the database
var originalTeamInQuestion = (from b in db.Teams
                                where b.Team1 == "testTeam"
                                select b).FirstOrDefault();

// Create Team object with the some changes
var team = new
{
    ID = originalTeamInQuestion.ID,
    Team1 = "changedTestTeam",
    ShowDDL = false,
};

// This is the API call which sends a PUT with only the parameters from team
var teamToUpdate = team.PutToJObject(baseUrl + apiCall, userAccount.token);

// Look for items in the database
var changedTeamInQuestion = (from b in db.Teams
                                where b.Team1 == "changedTestTeam"
                                select b).FirstOrDefault();

// This Assert succeeds and shows that changes have taken place
Assert.AreEqual(team.Team1, changedTeamInQuestion.Team1);

// This Assert is failing since no Email information is being sent
// and the binding model assigns it to Null since it didn't get that 
// as part of the PUT and overrides the object on update.
Assert.AreEqual(originalTeamInQuestion.Email, changedTeamInQuestion.Email);

Any ideas on some alternative approaches to this? We've thought of asking the client to first get the whole object by making a GET call to the API and then altering the object, but if a client doesn't follow that protocol they can very dangerously wipe out sensitive data.

Upvotes: 2

Views: 2596

Answers (1)

cal5barton
cal5barton

Reputation: 1616

I've implemented a static class that will take the enity object and update only the dirty properties of the entity. This allows for end users to explicitly set values to null if needed.

public static class DirtyProperties
{
    public static T ToUpdatedObject<T>(T entityObject)
    {
        return UpdateObject(entityObject,GetDirtyProperties());
    }

    private static Dictionary<string,object>GetDirtyProperties()
    {
        //Inspects the JSON payload for properties explicitly set.
        return JsonConvert.DeserializeObject<Dictionary<string, object>>(new StreamReader(HttpContext.Current.Request.InputStream).ReadToEnd());
    }

    private static T UpdateObject<T>(T entityObject, Dictionary<string, object> properties)
    {

        //Loop through each changed properties and update the entity object with new values
        foreach (var prop in properties)
        {
            var updateProperty = entityObject.GetType().GetProperty(prop.Key);// Try and get property

            if (updateProperty != null)
            {
                SetValue(updateProperty, entityObject, prop.Value);
            }
        }

        return entityObject;
    }

    private static void SetValue(PropertyInfo property, object entity, object newValue)
    {
        //This method is used to convert binding model properties to entity properties and set the new value
        Type t = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
        object safeVal = (newValue == null) ? null : Convert.ChangeType(newValue, t);

        property.SetValue(entity, safeVal);
    }
}

Upvotes: 1

Related Questions