Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;

like image 536
Rob Perkins Avatar asked Oct 19 '21 23:10

Rob Perkins


People also ask

What can you do with Microsoft Graph?

Microsoft Graph provides access to data stored across Microsoft 365 services. Custom applications can use the Microsoft Graph API to connect to data and use it in custom applications to enhance organizational productivity.

Can I turn Microsoft Graph on?

While you are still signed in to the Microsoft 365 Admin Portal, select the Settings > Org settings menu item. Select the Microsoft Graph Data Connect service. Select the checkbox that says turn Microsoft Graph Data Connect on or off for your entire organization to enable Data Connect.


2 Answers

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;
like image 125
weichch Avatar answered Oct 19 '22 10:10

weichch


You may find the HttpProvider of the GraphServiceClient helpful in this scenario:

        var listItemsCollectionRequest = graphServiceClient
         .Sites["<guid>"]
         .Lists["<nameOfList>"]
         .Items
         .Request()
         .Header("Accept", "application/json;odata.metadata=none")
         .Select("fields,id")
         .Expand("fields");

        using (var requestMessage = listItemsCollectionRequest.GetHttpRequestMessage())
        {
            using var responseMessage = await graphServiceClient.HttpProvider.SendAsync(requestMessage);

            //deserialize the response body into DataItem
        }

By using the HttpProvider you can directly work with the response from the Graph API and deserialize the response body into your custom class.

like image 1
Marc Avatar answered Oct 19 '22 10:10

Marc