The purpose of this document is to outline any breaking change and migration work SDK users might run into while upgrading to the v5 of the SDK.
V5 of the Microsoft Graph .NET SDK moves to the new code generator Kiota to provide a better user experience for the SDK users as well as a number of new features made possible by these changes.
The following section lists out the breaking changes requiring code changes from SDK users.
The types in the sdk are now organized into namespaces reflecting their usage as compared to all types being present in the single Microsoft.Graph
namespace and therefore makes it easier to consume multiple libraries(e.g v1.0 and beta) in the same application.
This therefore comes with the following changes,
- The v1.0 service library uses
Microsoft.Graph
as its root namespace. - The beta service library uses
Microsoft.Graph.Beta
as its root namespace. - Model types are now in the
Microsoft.Graph.Models
/Microsoft.Graph.Beta.Models
namespaces. - RequestBuilder and RequestBody types reside in namespaces relative to the path they are calling. e.g. The
SendMailPostRequestBody
type will reside in theMicrosoft.Graph.Beta.Me.SendMail/Microsoft.Graph.Me.SendMail
namespace if you are sending a mail via theclient.Me.SendMail.PostAsync(sendMailPostRequestBody)
path using the request builders
The GraphServiceClient
constructor accepts instances of TokenCredential
from Azure.Identity similar to previous library version as follows
var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveBrowserCredentialOptions);
var graphServiceClient = new GraphServiceClient(interactiveBrowserCredential);
In place of the DelegateAuthenticationProvider, custom authentication flows can be done creating an implementation of IAccessTokenProvider, and using with the BaseBearerTokenAuthenticationProvider from the Kiota abstractions as follows
public class TokenProvider : IAccessTokenProvider
{
public Task<string> GetAuthorizationTokenAsync(Uri uri, Dictionary<string, object> additionalAuthenticationContext = default,
CancellationToken cancellationToken = default)
{
var token = "token";
// get the token and return it in your own way
return Task.FromResult(token);
}
public AllowedHostsValidator AllowedHostsValidator { get; }
}
Then create the GraphServiceClient
as follows
var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new TokenProvider());
var graphServiceClient = new GraphServiceClient(authenticationProvider);
Authentication using the graph client is no longer handled in the HttpClient middleware pipeline. Therefore, using the
GraphServiceClient(httpClient)
constructor will assume that the passed httpClient has already been configured to handle authentication in its pipeline. Otherwise, passing an instance ofIAuthenticationProvider
to the constructor (GraphServiceClient(httpClient, authenticationProvider)
) will make authenticated requests if the passed HttpClient is not already configured.
The RequestInformation
class is now used to represent requests in the SDK and the IBaseRequest
is dropped. Using the fluent API, you can always get an instance of the RequestInformation
as follows.
// Get the requestInformation to make a GET request
var requestInformation = graphServiceClient
.DirectoryObjects
.ToGetRequestInformation();
// Get the requestInformation to make a POST request
var directoryObject = new DirectoryObject()
{
Id = Guid.NewGuid().ToString()
};
var requestInformation = graphServiceClient
.DirectoryObjects
.ToPostRequestInformation(directoryObject);
In previous versions, of the SDK, calls involved the calling of Request()
in the request API as follows
var user = await graphServiceClient
.Me
.Request() // this is removed
.GetAsync();
A similar call in the V5 will have the Request()
section removed to be called as below.
var user = await graphServiceClient
.Me
.GetAsync();
V5 of the SDK simplifies the request methods to simply reflect the HTTP methods being used. This therefore means that
UpdateAsync()
methods are nowPatchAsync()
AddAsync()
methods are nowPostAsync()
To pass headers, the HeaderOption
class is no longer used. Headers are added using the requestConfiguration
modifier as follows
var user = await graphServiceClient
.Users["{user-id}"]
.GetAsync(requestConfiguration => requestConfiguration.Headers.Add("ConsistencyLevel","eventual"));
To pass query Options, the QueryOption
class is no longer used. Query options are set using the requestConfiguration
modifier as follows
var user = await graphServiceClient
.Users["{user-id}"]
.GetAsync(requestConfiguration => requestConfiguration.QueryParameters.Select = new string[] { "id", "createdDateTime"});
Example with multiple parameters
var groups = await graphServiceClient
.Groups
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Select = new string[] { "id", "createdDateTime","displayName"};
requestConfiguration.QueryParameters.Expand = new string[] { "members" };
requestConfiguration.QueryParameters.Filter = "startswith(displayName%2C+'J')";
});
To pass per-request options to the default http middleware to configure actions like redirects and retries, this can be done using the requestConfiguration
by adding an IRequestOption
instance to the Options
collection. For example, adding a RetryHandlerOption
instance to configure the retry handler option as below.
var retryHandlerOption = new RetryHandlerOption
{
MaxRetry = 7,
ShouldRetry = (delay,attempt,message) => true
};
var user = await graphClient.Me.GetAsync(requestConfiguration => requestConfiguration.Options.Add(retryHandlerOption));
Other IRequestOption
instances provided by default include the following and their source can be found here
RetryHandlerOption
- for configuring the retry handler to customise request retriesRedirectHandlerOption
- for configuring the redirect handler to customise request redirectsChaosHandlerOption
- for configuring the chaos handler to customise simulated chaos when testing with mock responses
The per-request options object can be used to obtain the native HttpReponseMessage
from the request to override the default response handling of the request builders using the ResponseHandlerOption
as below. This can be used in scenarios where one wished to access the native response object or customize the response handling by creating and passing an instance of IResponseHandler.
var nativeResponseHandler = new NativeResponseHandler();
await graphClient.Me.GetAsync(requestConfiguration => requestConfiguration.Options.Add(new ResponseHandlerOption(){ ResponseHandler = nativeResponseHandler }));
var responseMessage = nativeResponseHandler.Value as HttpResponseMessage;
Querying for collections are done as follows and resembles the response from API.
var usersResponse = await graphServiceClient
.Users
.GetAsync(requestConfiguration => requestConfiguration.QueryParameters.Select = new string[] { "id", "createdDateTime"});
List<User> userList = usersResponse.Value;
To iterate through page collections, use the pageIterator as follows
var usersResponse = await graphServiceClient
.Users
.GetAsync(requestConfiguration => {
requestConfiguration.QueryParameters.Select = new string[] { "id", "createdDateTime" };
requestConfiguration.QueryParameters.Top = 1;
});
var userList = new List<User>();
var pageIterator = PageIterator<User,UserCollectionResponse>.CreatePageIterator(graphServiceClient,usersResponse, (user) => { userList.Add(user); return true; });
await pageIterator.IterateAsync();
Given the API guidance here, the metadata used to generate the SDK does not include the $skipToken
and $deltaToken
query parameters.
The entire url is documented as opaque and the change of the url structure (e.g. using alternative query parameters) is not considered a breaking change.
Due to this, urls returned from a delta response should be used entirely by either
- Using the inbuilt request builders to make subsequent requests from the
@odata.nexlink
(OdataDeltaLink property) or@odata.deltaLink
(OdataNextLink property) returned from the delta request.
// make the first request
var deltaResponse = await graphClient.Groups.Delta.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string[] { "displayName", "description", "mailNickname" };
});
// use the deltaResponse.OdataDeltaLink/deltaResponse.OdataNextLink to make the next request.
var deltaRequest = new Microsoft.Graph.Beta.Groups.Delta.DeltaRequestBuilder(deltaResponse.OdataDeltaLink, graphClient.RequestAdapter);
var secondDeltaResponse = await deltaRequest.GetAsync();
- Using the PageIterator
//fetch the first page of groups
var deltaResponse = await graphClient.Groups.Delta.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string[] { "displayName", "description", "mailNickname" };
});
// create a list to hold the groups
var groups = new List<Group>();
// create a page iterator to iterate through the pages of the response
var pageIterator = PageIterator<Group, Microsoft.Graph.Beta.Groups.Delta.DeltaResponse>.CreatePageIterator(graphClient, deltaResponse, group =>
{
groups.Add(group);
return true;
});
// This will iterate follow through the odata.nextLink until the last page is reached with an odata.deltaLink
await pageIterator.IterateAsync();
if (pageIterator.State == PagingState.Delta)
{
await Task.Delay(30000);// wait for some time for changes to occur.
// call delta again with the deltaLink to get the next page of results
Console.WriteLine("Calling delta again with deltaLink");
Console.WriteLine("DeltaLink url is: " + pageIterator.Deltalink);
await pageIterator.IterateAsync();
}
Errors and exceptions from the new generated version will be exception classed derived from the ApiException class from the Kiota abstrations library. Typically, this will be an instance of OdataError and can be handled as below.
try
{
await graphServiceClient.Me.PatchAsync(user);
}
catch (ODataError odataError)
{
Console.WriteLine(odataError.Error.Code);
Console.WriteLine(odataError.Error.Message);
throw;
}
The current CSDL to OpenAPI conversion process avoids generation of redundant paths which impacts request builders for driveItems. To mitigate this paths should be available through alternative paths as documented in the reference documentation as seen here.
Examples of using alternative paths are as shown below.
- List children from a user's drive.
// Get the user's driveId
var driveItem = await graphServiceClient.Me.Drive.GetAsync();
var userDriveId = driveItem.Id;
// List children in the drive
var children = await graphServiceClient.Drives[userDriveId].Items["itemId"].Children.GetAsync();
NOTE: /drive/root is a shorthand for /drive/items/root so the
itemId
can be replaced withroot
to make a call to get the root folder.
// List children in the root drive
var children = await graphServiceClient.Drives[userDriveId].Items["root"].Children.GetAsync();
- List children from a site's drive.
// Get the site's driveId
var driveItem = await graphServiceClient.Sites["site-id"].Drive.GetAsync();
var siteDriveId = driveItem.Id;
// List children in the drive
var children = await graphServiceClient.Drives[siteDriveId].Items["itemId"].Children.GetAsync();
- List children from a groups's drive.
// Get the group's driveId
var driveItem = await graphServiceClient.Groups["group-id"].Drive.GetAsync();
var groupDriveId = driveItem.Id;
// List children in the drive
var children = await graphServiceClient.Drives[groupDriveId].Items["itemId"].Children.GetAsync();
To upload a small file (remember the size should not exceed 4mb according to the docs) and at the same time, set the conflictBehavior
instance attribute you'll need to do it this way:
var requestInformation = graphClient.Drives[drive.Id.ToString()].Root.ItemWithPath("MediaMeta.xml").Content.ToPutRequestInformation(file);
requestInformation.URI = new Uri(requestInformation.URI.OriginalString +"[email protected]=rename");
var result = await graphClient.RequestAdapter.SendAsync<DriveItem>(requestInformation, DriveItem.CreateFromDiscriminatorValue);
To upload large files, the method is slightly different.
The backing store allows multiple things like dirty tracking of changes, making it possible to get an object from the API, update a property, send that object back with only the changed property and not the full objects.
This has the added advantage in that SDK user can simply get an object from the API and set a property to null and send back the object without having to use known workarounds where setting properties to null would require to be placed in the additionalData bag.
// get the object
var @event = await graphServiceClient
.Me.Events["event-id"]
.GetAsync();
// the backing store will keep track that the property change and send the updated value.
@event.Recurrence = null;// set to null
// update the object
await graphServiceClient.Me.Events["event-id"]
.PatchAsync(@event);
To add parameters to an odata action, the SDK uses parameter objects rather than using function overloads to reduce likelihood of the SDK shpping breaking changes. In the v4 SDK, calling a function/action would look like this.
//var message = ....
//var saveToSentItems = ...
await graphClient.Me
.SendMail(message,saveToSentItems)
.Request()
.PostAsync();
This changes to
//var message = ....
//var saveToSentItems = ...
var body = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody
{
Message = message,
SaveToSentItems = saveToSentItems
};
await graphServiceClient.Me
.SendMail
.PostAsync(body);
Apart from passing instances of HttpRequestMessage
, batch requests support the passing of RequestInformation
instances as follows.
var requestInformation = graphServiceClient
.Users
.ToGetRequestInformation();
// create the batch request
var batchRequestContent = new BatchRequestContent(graphServiceClient);
// add one or more steps (up to 20, or check below)
var requestStepId = await batchRequestContent.AddBatchRequestStepAsync(requestInformation);
// send and get back response
var batchResponseContent = await graphServiceClient.Batch.PostAsync(batchRequestContent);
var usersResponse = await batchResponseContent.GetResponseByIdAsync<UserCollectionResponse>(requestStepId);
List<User> userList = usersResponse.Value;
Find failing responses (and retry)
var statusCodes = await batchResponseContent.GetResponsesStatusCodesAsync();
// all the responses are successfull?
var allReponsesSuccessFull = statusCodes.Any( x => !BatchResponseContent.IsSuccessStatusCode(x.Value));
// the responses with a
var rateLimitedResponses = statusCodes.Where(x => x.Value == (HttpStatusCode)429);
// maybe retry those rate limited?
var retryBatch = batchRequestContent.NewBatchWithFailedRequests(rateLimitedResponses);
var retryResponse = await graphServiceClient.Batch.PostAsync(retryBatch);
Automatically manage batch size with BatchRequestContentCollection
.
The sample above uses the BatchRequestContent
which has a limit of max. 20 combined requests. This means you'll need to manage the batch size yourself if you go beyond the batch limit of 20 requests.
// Replace BatchRequestContent with BatchRequestContentCollection and you're good to go.
var batchRequestContent = new BatchRequestContentCollection(graphServiceClient);
// or with a set batch size
// var batchRequestContent = new BatchRequestContentCollection(graphServiceClient, 4);
// Add "unlimited" requests, but don't use "DependsOn", you cannot be sure they will be in the same request and thus fail.
var requestStepId = await batchRequestContent.AddBatchRequestStepAsync(requestInformation);
// Execute the request like before and use the response like before.
Using batched requests can make your code a lot faster, if you need to query several endpoints at once or if you're creating/deleting a lot of items at the same time. Check out the sample code in this repository for a complete sample on batching and experience yourself how much faster your application can process those requests.
The request builders are now enriched with Count
sections where applicable to enable the use of the $count segment
var count = await graphServiceClient.Users.Count.GetAsync(requestConfiguration => requestConfiguration.Headers.Add("ConsistencyLevel","eventual"));
This addresses the workarounds that SDK users had to make in order to call $count which looked as follows in previous SDK versions(ref here)
string requestUrl = graphClient.Users.AppendSegmentToRequestUrl("$count");
Option [] options = new Option[] { new HeaderOption("ConsistencyLevel", "eventual") };
HttpResponseMessage responseMessage = await new UserRequest(requestUrl, graphClient, options)
.SendRequestAsync(null,CancellationToken.None);
string userCount = await responseMessage.Content.ReadAsStringAsync();
The request builders are now enriched with segments to enable requesting a specific type in the event an API endpoint supports the odata cast functionality.
An example is fetching the members of a group who are of the type User
which would be done as below.
var usersInGroup = await graphServiceClient.Groups["group-id"].Members.GraphUser.GetAsync();
Similarly, members of the group of type Application
would be done as below.
var applicationsInGroup = await graphServiceClient.Groups["group-id"].Members.GraphApplication.GetAsync();