Microsoft Graph .NET SDK v5
The Microsoft Graph .NET SDK v5 is a library that allows developers to interact with the Microsoft Graph API using .NET. The SDK is autogenerated from metadata by the tool called Kiota.
The SDK generated by Kiota uses request builder pattern to easily construct and execute requests.
Limitations
Even though the SDK v5 was released more than 2 years ago, it's impossible to call some endpoints either due to missing metadata information about those endpoints or because it was intended to reduce the size.
Well-known examples are some endpoints for the Workbook API, which allows to interact with Excel files.
There can be also a case when you can't for some reason to update the SDK to the latest version, but you need to call some new Graph API endpoint.
How to deal with that?
Custom Request Builder
The solution is easy. You can create a custom request builder to call any endpoint with the Microsoft Graph .NET SDK v5.
Let's start by creating a new class that inherits from the BaseRequestBuilder
class.
public class CustomRequestBuilder : BaseRequestBuilder
{
public class CustomRequestBuilderGetQueryParameters
{
[QueryParameter("%24count")]
public bool? Count { get; set; }
[QueryParameter("%24expand")]
public string[]? Expand { get; set; }
[QueryParameter("%24filter")]
public string? Filter { get; set; }
[QueryParameter("%24orderby")]
public string[]? Orderby { get; set; }
[QueryParameter("%24search")]
public string? Search { get; set; }
[QueryParameter("%24select")]
public string[]? Select { get; set; }
[QueryParameter("%24top")]
public int? Top { get; set; }
}
public CustomRequestBuilder(string path, IRequestAdapter requestAdapter)
: base(requestAdapter, $"{{+baseurl}}/{WebUtility.UrlEncode(path)}{{?%24count,%24expand,%24filter,%24orderby,%24search,%24select,%24top}}", new Dictionary<string, object>())
{
}
}
The constructor takes two parameters:
path
: The path of the endpoint you want to call.requestAdapter
: The request adapter that will be used to send the request.
The inner class CustomRequestBuilderGetQueryParameters
contains the query parameters that can be used in the request. You can add more query parameters as needed, but don't forget to add those parameters when calling the base constructor of the BaseRequestBuilder
class.
GET Request
To send a GET request, you can create a method that takes the query parameters as input and returns the response.
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
return requestInfo;
}
public async Task<T> GetAsync<T>(ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null, string contentType = "application/json", CancellationToken cancellationToken = default)
where T : IParsable
{
var requestInfo = ToGetRequestInformation(requestConfiguration, contentType);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue }
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
}
public async Task<UntypedNode> GetAsync(Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null, string contentType = "application/json", CancellationToken cancellationToken = default)
{
return await GetAsync(UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, contentType, cancellationToken);
}
The ToGetRequestInformation
method creates a RequestInformation
object that represents an abstract HTTP request. The GetAsync<T>
method sends the GET request and returns the response as a deserialized object of type T
that implements the IParsable
interface. The GetAsync
method sends the GET request and returns the response as an untyped node.
POST Request
The methods for sending POST requests are similar to the GET request methods.
public RequestInformation ToPostRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PostAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null, string contentType = "application/json", CancellationToken cancellationToken = default)
where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPostRequestInformation(body, requestConfiguration, contentType);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue }
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
}
public async Task<UntypedNode> PostAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PostAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
The ToPostRequestInformation
method creates a RequestInformation
object that represents an abstract HTTP request. The PostAsync<T>
method sends the POST request and returns the response as a deserialized object of type T
that implements the IParsable
interface. The PostAsync
method sends the POST request and returns the response as an untyped node.
PATCH, PUT Requests
The methods for sending PATCH and PUT requests are similar to the POST request methods.
public RequestInformation ToPatchRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.PATCH, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PatchAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json",
CancellationToken cancellationToken = default) where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPatchRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(false);
}
public async Task<UntypedNode> PatchAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PatchAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
public RequestInformation ToPutRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.PUT, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PutAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json",
CancellationToken cancellationToken = default) where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPutRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(false);
}
public async Task<UntypedNode> PutAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PutAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
Example
Let's try some examples. The SDK doesn't allow to call the GET /me/drive/items/{item-id}
endpoint.
Create a new instance of the GraphServiceClient
as usual:
var graphClient = new GraphServiceClient(credential);
You need to first get drive of the user:
var drive = graphClient.Me.Drive.GetAsync();
Then use the drive id to get the drive item:
var driveItem = graphClient.Drives[drive.Id].Items[itemId].GetAsync();
With the custom request builder, you can call the endpoint directly:
var customRequestBuilder = new CustomRequestBuilder($"me/drive/items/{itemId}", graphClient.RequestAdapter);
var driveItem = await customRequestBuilder.GetAsync(DriveItem.CreateFromDiscriminatorValue, rc =>
{
rc.QueryParameters.Select = ["id", "name"];
rc.QueryParameters.Expand = ["versions"];
});
Another example
If you want to get the borders of a workbook range, you will find out that the SDK doesn't generate a code to retrieve or update the workbook range border.
You can use the custom request builder to call the endpoint directly:
me/drive/items/{drive_item_id}/workbook/worksheets/{sheet_id}/range/format/borders
Code:
var path = "me/drive/items/{drive_item_id}/workbook/worksheets/{sheet_id}/range/format/borders";
var customRequestBuilder = new CustomRequestBuilder(path, graphClient.RequestAdapter);
var result = await customRequestBuilder.GetAsync();
The value of result is an instance of UntypedObject
, which represents a generic, untyped response from the Microsoft Graph API. This indicates that the response was not deserialized into a strongly-typed object, because no specific type exists in the SDK.
If you want to update the border of a workbook range:
var path = "me/drive/items/{drive_item_id}/workbook/worksheets/{sheet_id}/range/format/borders/EdgeTop";
var customRequestBuilder = new CustomRequestBuilder(path, graphClient.RequestAdapter);
var rangeBorder = new UntypedObject(new Dictionary<string, UntypedNode>
{
{"color", new UntypedString("#00FF00") },
{"style", new UntypedString("None") },
{"sideIndex", new UntypedString("EdgeTop") },
{"weight", new UntypedString("Thin") }
});
var result = await customRequestBuilder.PatchAsync(rangeBorder);
Conclusion
In this article, I showed you how to create a custom request builder to call any endpoint with the Microsoft Graph .NET SDK v5.
The full code of the CustomRequestBuilder
class:
public class CustomRequestBuilder : BaseRequestBuilder
{
public class CustomRequestBuilderGetQueryParameters
{
[QueryParameter("%24count")]
public bool? Count { get; set; }
[QueryParameter("%24expand")]
public string[]? Expand { get; set; }
[QueryParameter("%24filter")]
public string? Filter { get; set; }
[QueryParameter("%24orderby")]
public string[]? Orderby { get; set; }
[QueryParameter("%24search")]
public string? Search { get; set; }
[QueryParameter("%24select")]
public string[]? Select { get; set; }
[QueryParameter("%24top")]
public int? Top { get; set; }
}
public CustomRequestBuilder(string path, IRequestAdapter requestAdapter)
: base(requestAdapter, $"{{+baseurl}}/{WebUtility.UrlEncode(path)}{{?%24count,%24expand,%24filter,%24orderby,%24search,%24select,%24top}}", new Dictionary<string, object>())
{
}
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
return requestInfo;
}
public async Task<T> GetAsync<T>(ParsableFactory<T> parsableFactory,
Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null,
string contentType = "application/json",
CancellationToken cancellationToken = default)
where T : IParsable
{
var requestInfo = ToGetRequestInformation(requestConfiguration, contentType);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue }
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
}
public async Task<UntypedNode> GetAsync(Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null, string contentType = "application/json", CancellationToken cancellationToken = default)
{
return await GetAsync(UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, contentType, cancellationToken);
}
public RequestInformation ToPostRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PostAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = null, string contentType = "application/json", CancellationToken cancellationToken = default)
where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPostRequestInformation(body, requestConfiguration, contentType);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue }
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
}
public async Task<UntypedNode> PostAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PostAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
public RequestInformation ToPatchRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.PATCH, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PatchAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json",
CancellationToken cancellationToken = default) where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPatchRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(false);
}
public async Task<UntypedNode> PatchAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PatchAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
public RequestInformation ToPutRequestInformation(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json")
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.PUT, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", contentType);
requestInfo.SetContentFromParsable(RequestAdapter, contentType, body);
return requestInfo;
}
public async Task<T> PutAsync<T>(IParsable body, ParsableFactory<T> parsableFactory, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default, string contentType = "application/json",
CancellationToken cancellationToken = default) where T : IParsable
{
_ = body ?? throw new ArgumentNullException(nameof(body));
var requestInfo = ToPutRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "XXX", ODataError.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync(requestInfo, parsableFactory, errorMapping, cancellationToken).ConfigureAwait(false);
}
public async Task<UntypedNode> PutAsync(IParsable body, Action<RequestConfiguration<CustomRequestBuilderGetQueryParameters>> requestConfiguration = default,
CancellationToken cancellationToken = default)
{
return await PutAsync(body, UntypedNode.CreateFromDiscriminatorValue, requestConfiguration, cancellationToken: cancellationToken);
}
}