Search code examples
c#jsonmicrosoft-graph-apisharepoint-onlinesharepoint-list

Transforming Microsoft Graph ListItem Output to a corresponding C# type


The work of transforming JSON data into a typed data model through seems to be made much more complex by the "help" the combination of SharePoint and MS Graph offer. :-)

I have a SharePoint List in Microsoft 365 that I'm accessing through the Graph API in C#, where the query destination is a typed class with properties identical to the SharePoint List Column Properties.

The ListItem class Graph API returns the results in the a Fields.AdditionalData of type Dictionary<string,object{System.Text.Json.JsonElement}> It needs to become an IEnumerable<DataItem>, which I can do by taking the List from the query result through a Serialize/Deserialize round trip, as below:

var backToJSON = ListItems.Select(o => System.Text.Json.JsonSerializer.Serialize(o.Fields.AdditionalData));
var stronglyTypedItems = backToJSON.Select(jsonO => System.Text.Json.JsonSerializer.Deserialize<DataItem>(jsonO));

Is there a way to do this, either with smarter OData or something in Graph API I haven't seen, without taking what used to be JSON and sending it back through JSON Serializers twice?

More details below: Sample output JSON from Graph Explorer, where value contains an array of :

"value" : [ 
    { "id": "1001, 
      "fields": { 
        "Column" : "true", 
        "Column2" : "value2", 
        "Column3" : "65" 
      } 
    }, 
    { "id": "1002, 
      "fields": { 
  <and so forth until the array terminates>
  ]
}

Corresponding C# Class (literally built using "Paste JSON as class"):

Public class DataItem {
  public bool Column {get; set;}
  public string Column2 {get; set;}
  public int Column3 {get; set;}
}

The "Helper" classes in the C# Graph API deliver mostly transformed into the array of fields I actually need:

        private static GraphServiceClient graphClient;

        public static IListItemsCollectionRequest LicenseExpirationsList => graphClient
            .Sites["<guid>"]
            .Lists["<nameOfList>"].Items
            .Request()
            .Header("Accept", "application/json;odata.metadata=none")
            .Select("fields,id")
            .Expand("fields");

            var ListItems = (await GraphHelper.LicenseExpirationsList.GetAsync()).CurrentPage;


// JSON round tripping through JSONSerializer to get the strong type...
// But why? ListItems.Fields.AdditionalData is a Dictionary of JSON elements in the first place!

            var backToJSON = ListItems.Select(o => System.Text.Json.JsonSerializer.Serialize(o.Fields.AdditionalData));
            var stronglyTypedItems = backToJSON.Select(jsonO => System.Text.Json.JsonSerializer.Deserialize<DataItem>(jsonO));
 

            return stronglyTypedItems;


Solution

  • You could customize the client's JSON serialization to return a derived type of default FieldValueSet.

    First, define your own extended FieldValueSet:

    public class FieldValueSetWithDataItem : FieldValueSet
    {
        public bool Column { get; set; }
        public string Column2 { get; set; }
        public int Column3 { get; set; }
    }
    

    Second, implement your own JSON converter:

    class CustomFieldValueSetJsonConverter : JsonConverter<FieldValueSet>
    {
        private static readonly JsonEncodedText ODataTypeProperty 
            = JsonEncodedText.Encode("@odata.type");
        private static readonly JsonEncodedText IdProperty 
            = JsonEncodedText.Encode("id");
        private static readonly JsonEncodedText ColumnProperty 
            = JsonEncodedText.Encode("Column");
        private static readonly JsonEncodedText Column2Property 
            = JsonEncodedText.Encode("Column2");
        private static readonly JsonEncodedText Column3Property
            = JsonEncodedText.Encode("Column3");
    
        public override FieldValueSet Read(ref Utf8JsonReader reader,
            Type typeToConvert, JsonSerializerOptions options)
        {
            var result = new FieldValueSetWithDataItem();
            using var doc = JsonDocument.ParseValue(ref reader);
            var root = doc.RootElement;
    
            foreach (var element in root.EnumerateObject())
            {
                if (element.NameEquals(ODataTypeProperty.EncodedUtf8Bytes))
                {
                    result.ODataType = element.Value.GetString();
                }
                else if (element.NameEquals(IdProperty.EncodedUtf8Bytes))
                {
                    result.Id = element.Value.GetString();
                }
                else if (element.NameEquals(ColumnProperty.EncodedUtf8Bytes))
                {
                    result.Column = element.Value.GetBoolean();
                }
                else if (element.NameEquals(Column2Property.EncodedUtf8Bytes))
                {
                    result.Column2 = element.Value.GetString();
                }
                else if (element.NameEquals(Column3Property.EncodedUtf8Bytes))
                {
                    result.Column3 = element.Value.GetInt32();
                }
                else
                {
                    // Capture unknown property in AdditionalData
                    if (result.AdditionalData is null)
                    {
                        result.AdditionalData = new Dictionary<string, object>();
                    }
                    result.AdditionalData.Add(element.Name, element.Value.Clone());
                }
            }
    
            return result;
        }
    
        public override void Write(Utf8JsonWriter writer,
            FieldValueSet value, JsonSerializerOptions options)
        {
            // To support roundtrip serialization:
            writer.WriteStartObject();
    
            writer.WriteString(ODataTypeProperty, value.ODataType);
            writer.WriteString(IdProperty, value.Id);
    
            if (value is FieldValueSetWithDataItem dataItem)
            {
                writer.WriteBoolean(ColumnProperty, dataItem.Column);
                writer.WriteString(Column2Property, dataItem.Column2);
                writer.WriteNumber(Column3Property, dataItem.Column3);
            }
    
            if (value.AdditionalData is not null)
            {
                foreach (var kvp in value.AdditionalData)
                {
                    writer.WritePropertyName(kvp.Key);
                    ((JsonElement)kvp.Value).WriteTo(writer);
                }
            }
            
            writer.WriteEndObject();
        }
    }
    

    Last, use the JSON converter when making your request:

    // Use custom JSON converter when deserializing response
    var serializerOptions = new JsonSerializerOptions();
    serializerOptions.Converters.Add(new CustomFieldValueSetJsonConverter());
    
    var responseSerializer = new Serializer(serializerOptions);
    var responseHandler = new ResponseHandler(responseSerializer);
    
    var request = (ListItemsCollectionRequest)client.Sites[""].Lists[""].Items.Request();
    
    var listItems = await request
        .WithResponseHandler(responseHandler)
        .GetAsync();
    

    To access your column values:

    var col3 = ((FieldValueSetWithDataItem)listItem.Fields).Column3;