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 beta 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')";
});
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();
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();
- 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();
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 content
var batchRequestContent = new BatchRequestContent(graphServiceClient);
// add steps
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;
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();