Search code examples
c#apipropertiesmappingpropertyinfo

Issue with PropertyInfo getProperty() Method


request.Data
{
    "jurisdictionCode": "California",
    "claimId": 123654,
    "claimGroupID": 12,
    "claimXref": "32145",
    "serviceXref": "Test",
    "claimStart": "2021-07-30T13:20:15.338Z",
    "claimEnd": "2021-07-30T13:20:15.338Z",
    "status": 5,
    "creationTimestamp": "2021-07-30T13:20:15.338Z",
    "touchTimestamp": "2021-07-30T13:20:15.338Z",
    "filingSource": 7,
    "userName": "test",
    "exportTs": "2021-07-30T13:20:15.338Z",
    "payerXref": "test",
    "dtbatchExportTs": "2021-07-30T13:20:15.338Z"
  }


public class scaffolded_model
{
[Key]
[StringLength(10)]
public string JurisdictionCode { get; set; }

[Key]
public long ClaimID { get; set; }   

public long ClaimGroupID { get; set; }  

[Required]
[StringLength(64)]
public string ClaimXRef { get; set; }   

[Required]
[StringLength(64)]
public string ServiceXRef { get; set; } 

[Column(TypeName = "datetime")]
public DateTime ClaimStart { get; set; }    

[Column(TypeName = "datetime")]
public DateTime ClaimEnd { get; set; }

public int Status { get; set; }

[Column(TypeName = "datetime")]
public DateTime CreationTimestamp { get; set; } 

[Column(TypeName = "datetime")]
public DateTime TouchTimestamp { get; set; }

public int FilingSource { get; set; }

[Required]
[StringLength(256)]
public string UserName { get; set; }

[Key]
[Column(TypeName = "datetime")]
public DateTime ExportTS { get; set; }

[Required]
[StringLength(64)]
public string PayerXRef { get; set; }

[Column(TypeName = "datetime")]
public DateTime DTBatchExportTS { get; set; }
}

Code:

    var data = JsonSerializer.Serialize(request.Data);

Dictionary<string, JsonElement> result = (Dictionary<string, JsonElement>)JsonSerializer.Deserialize(data, typeof(Dictionary<string, JsonElement>));

foreach (var item in result)
{
    PropertyInfo pi = scaffolded_model
        .GetType()
        .GetProperty(item.Key, BindingFlags.Instance | BindingFlags.Public);
   
    if (pi == null)
    {
        _logger.LogInformation("Bad Field");
        continue;
    }
    pi.SetValue(scaffolded_model, item.Value);
}

I have been having issues with using the GetProperty() method to match and populate values from a json request coming in as request.Data and an Empty model called scaffolded_model. As far as I can tell, both sets of data are setup correctly. The code should run through each value in the request, match it by item.key in the empty model and populate the matching key with the value. item.key comes up empty everytime. I have tried different bindings, etc. If I hard code the first item.key coming through as JurisdictionCode, it grabs the value and populates it correctly. So everything is working, if the item.key would populate.

Thanks for looking and for all of your help.

    [ApiVersion("1.0")]
    [HttpPost("v{version:apiVersion}/Submitclaim")]
    [Produces("application/json")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    public Task<IActionResult> Submitclaim(ClaimModel request)
    {
        var source = MethodBase.GetCurrentMethod().Name;
        IActionResult actionResult = null;
        
        using (LogContext.PushProperty("jx", request.JurisdictionCode))
        {
            try 
            {
                //var claim_data = JsonSerializer.Serialize(request);
                //Dictionary<string, JsonElement> result = (Dictionary<string, JsonElement>)JsonSerializer.Deserialize(claim_data, typeof(Dictionary<string, JsonElement>));

                API.CRUD.Claims.Model.Claim scaffolded_model = new API.CRUD.Claims.Model.Claim();

                JsonSerializer.Deserialize<scaffolded_model>(request);

                //foreach (var item in result)
                //{
                //    PropertyInfo pi = scaffolded_model
                //      .GetType()
                //      .GetProperty(
                //         item.Key,
                //         BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);

                //    if (pi == null)
                //    {
                //        _logger.LogInformation("Bad Field");
                //        continue;
                //    }
                //    pi.SetValue(
                //      scaffolded_model,
                //      Convert.ChangeType(item.Value.ToString(), pi.PropertyType));
                //}


            }
            catch (Exception ex)
            {
                _logger.LogError($"Exception failed: {ex.Message}");
                actionResult = Problem("Exception failed");
            }

        }

        return Task.FromResult(actionResult);

    }

Solution

  • If you have a good reason to implement custom JSON parsing via Reflection instead of using standard System.Text.Json or Newtonsoft.Json libraries, then there are couple issues that need to be resolved:

    • Type.GetProperty is case sensitive. By default it will not match jurisdictionCode property name to JurisdictionCode property. The BindingFlags.IgnoreCase flag should solve this issue.
    PropertyInfo pi = scaffolded_model
      .GetType()
      .GetProperty(
         item.Key, 
         BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
    
    • The second issue is related to the type conversion. PropertyInfo.SetValue does not perform any type conversions. The value that need to be set must match property type, otherwise TargetException will be thrown. At this time the code from the question always sets string value returned by item.Value.ToString() method to all properties. It will not work for long or DateTime properties. To resolve this issue, the Convert.ChangeType method can be used as the simplest option that will handle property types defined in the scaffolded_model class.
    pi.SetValue(
      scaffolded_model, 
      Convert.ChangeType(item.Value.ToString(), pi.PropertyType));
    

    With these two changes it will be possible to parse Json form example.


    But current code has some limitations:

    • It does not handle nulls. In case of string property, the empty string value will be assigned to the property instead of original null value. The nullable value types (e.g. long?) are not supported at all. To resolve this issue, current logic can be adjusted to check the JsonElement.ValueKind property for JsonValueKind.Null value.

    • Another issue is a DateTime type. With the current implementation all DateTime values will be adjusted to the local timezone, and Convert.ChangeType method does not provide any ability to control it. The replacement of DateTime time to DateTimeOffset will not work as Convert.ChangeType method does not support it. There is only option to check property type and perform manual conversion using, for example, DateTime.Parse method instead of Convert.ChangeType.

    This list can be continued. So in general case it is better to use standard libraries for parsing Json.