How to batch delete events in Office365 using C# Graph SDK - c#

I have a method that deletes multiple events. Currently the code is as following:
public async Task DeleteEvents(IEnumerable<string> eventExternalIds)
{
foreach(var eventExternalId in eventExternalIds)
{
await DeleteEvent(eventExternalId);
}
}
public async Task DeleteEvent(string eventExternalId)
{
await GraphClient
.Users[Username]
.Calendars[CalendarName]
.Events[eventExternalId]
.Request()
.DeleteAsync();
}
I would imagine it won't perform well with any significant number of id's to delete. Is there a way to delete them all in a batch(es) instead of each individually?

msgraph-sdk-dotnet v1.15.0 or above
For msgraph-sdk-dotnet version 1.15.0 or above the support for Batch request has been introduced via BatchRequestContent class
Example
//1. construct a Batch request
var batchRequestContent = new BatchRequestContent();
var step = 1;
foreach (var eventId in eventIds)
{
var requestUrl = graphClient
.Me
.Events[eventId]
.Request().RequestUrl;
var request = new HttpRequestMessage(HttpMethod.Delete, requestUrl);
var requestStep = new BatchRequestStep(step.ToString(), request, null);
batchRequestContent.AddBatchRequestStep(requestStep);
step++;
}
//2. Submit request
var batchRequest = new HttpRequestMessage(HttpMethod.Post, "https://graph.microsoft.com/v1.0/$batch");
batchRequest.Content = batchRequestContent;
await graphClient.AuthenticationProvider.AuthenticateRequestAsync(batchRequest);
var httpClient = new HttpClient();
var batchResponse = await httpClient.SendAsync(batchRequest);
//3. Process response
var batchResponseContent = new BatchResponseContent(batchResponse);
var responses = await batchResponseContent.GetResponsesAsync();
foreach (var response in responses)
{
if (response.Value.IsSuccessStatusCode)
{
//...
}
}
Issues
while targeting NetCore 2.1 or above or .NET Framework
NullReferenceException exception might occur, to address this issue
you could switch to 1.16.0-preview.1 (details)
Limitations
Note: A batch cannot hold more that 20 requests
msgraph-sdk-dotnet v1.14.0 or older
For previous versions, the following example demonstrates how to implement a support for Batch request:
var batchRequest = new BatchRequest();
foreach (var eventId in eventIds)
{
var request = graphClient.Me.Events[eventId].Request();
batchRequest.AddQuery(request,HttpMethod.Delete);
}
var responses = await graphClient.SendBatchAsync(batchRequest);
where BatchRequest is a custom class which adds support for JSON Batching

Related

Microsoft Botframework Send Proactive Message to Bot (and not to the User)

we are currently developing some automation with the botframework.
At some point in the conversation, we sent some data through a service bus for processing and wait for a response and then want to continue with the conversation. We already implemented the part where we wait for an response entry in the service bus subscription and then we want to send an Activity from type Event to the bot.
We did the same steps with the proactive message as described in other posts.
We are able to recreate the botclient and conversation reference and all, but in the end when we send the activity, we always send it to the user and not to the bot. But this doesn't trigger the "EventActivityPrompt".
The only way where we achieved the desired outcome was when we made a post to api/messages, but this is too complicated for our taste, and we are looking for an easier way over the botClient (or similar technology)
Has anyone some good ideas? :)
ServiceBusReceiver Message Processing:
private static async Task ProcessMessagesAsync(Message message, CancellationToken token)
{
// Process the message.
Console.WriteLine($"Received message: SequenceNumber:{message.SystemProperties.SequenceNumber} Body:{Encoding.UTF8.GetString(message.Body)}");
_logger?.LogInformation("Received message '{id}' with label '{label}' from queue.", message.MessageId, message.Label);
var data = JsonSerializer.Deserialize<BotCarLicensingOrderRpaRequest>(message.Body);
data.AdditionalData.TryGetValue("ServiceUrl", out var serviceUrl);
data.AdditionalData.TryGetValue("ChannelId", out var channelId);
data.AdditionalData.TryGetValue("BotId", out var botId);
data.AdditionalData.TryGetValue("UserId", out var userId);
data.AdditionalData.TryGetValue("ReplyToId", out var replyToId);
var conversationReference = _offTurnConversationService.CreateSyntheticConversationReference(
channelId?.ToString(),
data.ConversationId,
serviceUrl?.ToString());
conversationReference.User = new ChannelAccount()
{
Id = userId?.ToString(),
Role = "user"
};
conversationReference.Bot = new ChannelAccount
{
Id = botId?.ToString(),
Role = "bot"
};
var activity = (Activity)Activity.CreateEventActivity();
activity.Text = "success";
activity.ChannelId = channelId?.ToString();
activity.ServiceUrl = serviceUrl?.ToString();
activity.RelatesTo = conversationReference;
activity.Conversation = new ConversationAccount
{
Id = data.ConversationId
};
activity.ReplyToId = replyToId?.ToString();
activity.ApplyConversationReference(conversationReference, true);
// Complete the message so that it is not received again.
// This can be done only if the subscriptionClient is created in ReceiveMode.PeekLock mode (which is the default).
await _messageReceiver.CompleteAsync(message.SystemProperties.LockToken);
// This "works" but is complicated, as we have to set up a whole HTTP call
await _offTurnConversationService.SendActivityToBotAsync(activity);
// This just sends the Event to the user, no matter how I set up the conversation
// reference regarding From/Recipient
// And it doesn't help in continuing the conversation
await _offTurnConversationService.SendToConversationThroughPipelineAsync(
async (turnContext, cancellationToken) =>
{
await turnContext.SendActivityAsync(activity, cancellationToken: cancellationToken);
},
conversationReference);
// Note: Use the cancellationToken passed as necessary to determine if the subscriptionClient has already been closed.
// If subscriptionClient has already been closed, you can choose to not call CompleteAsync() or AbandonAsync() etc.
// to avoid unnecessary exceptions.
}
OffTurnConversationService:
public ConversationReference CreateSyntheticConversationReference(string channelId, string conversationId, string serviceUrl)
{
ArgumentGuard.NotNull(channelId, nameof(channelId));
ArgumentGuard.NotNull(conversationId, nameof(conversationId));
ArgumentGuard.NotNull(serviceUrl, nameof(serviceUrl));
if (string.IsNullOrEmpty(_botOptions.CurrentValue.BotId))
{
throw new InvalidOperationException("A valid bot id must be configured in your bot options in order to create a synthetic conversation reference.");
}
// WARNING: This implementation works for directline and webchat.
// Changes could be necessary for other channels.
var supportedChannels = new List<string>()
{
Channels.Directline,
Channels.Webchat
};
if (supportedChannels.Any(c => c.Equals(channelId, StringComparison.OrdinalIgnoreCase)))
{
_logger.LogWarning(
"The synthetic conversation reference created for channel {UsedChannel} might not work properly, " +
"because it's not supported and tested. Supported channels are {SupportedChannel}.",
channelId,
string.Join(",", supportedChannels));
}
var conversationReference = new ConversationReference()
{
Conversation = new ConversationAccount()
{
Id = conversationId
},
Bot = new ChannelAccount()
{
Id = _botOptions.CurrentValue.BotId,
Name = _botOptions.CurrentValue.BotId
},
ChannelId = channelId,
ServiceUrl = serviceUrl
};
return conversationReference;
}
public virtual async Task SendActivityToBotAsync(IActivity activity)
{
// Create the new request to POST to the client
var forwardRequest = new HttpRequestMessage()
{
RequestUri = new Uri(_botOptions.CurrentValue.ReplyServiceUrl),
Method = HttpMethod.Post,
};
// Change the host for the request to be the forwarding URL.
forwardRequest.Headers.Host = forwardRequest.RequestUri.Host;
// If the child bot is not running on local mode (no app-id/password),
// we're going send an authentication header.
OAuthResponse authToken = await GetTokenAsync(_botOptions.CurrentValue.MicrosoftAppId, _botOptions.CurrentValue.MicrosoftAppPassword);
forwardRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken.AccessToken);
// Altered activity to JSON content
var json = JsonConvert.SerializeObject(activity);
var content = new StringContent(json, Encoding.UTF8, "application/json");
forwardRequest.Content = content;
using var client = new HttpClient();
var response = await client.SendAsync(forwardRequest);
if (!response.IsSuccessStatusCode)
{
string message = $"Failed to send activity '{activity.Id}' to client bot. {response.ReasonPhrase}";
throw new Exception(message);
}
}
public virtual async Task SendToConversationThroughPipelineAsync(
BotCallbackHandler callback,
ConversationReference conversationReference)
{
ArgumentGuard.NotNull(callback, nameof(callback));
ArgumentGuard.NotNull(conversationReference, nameof(conversationReference));
// Avoiding 401 "Unauthorized" errors
TrustServiceUrl(conversationReference.ServiceUrl);
// Reuse adapter with its pipeline to send responses back to the user (like pro-active messages)
await ((BotAdapter)_botFrameworkHttpAdapter).ContinueConversationAsync(
_botOptions.CurrentValue.MicrosoftAppId,
conversationReference,
callback,
default);
}

File upload API endpoint broken after ASP.NET Core migration

I just migrated an ASP.NET WebAPI to ASP.NET Core 2.1 (I also tried 2.2). It contains a file upload route which receives a multipart request a binary file, with a known key / name.
My issue is that request.Form.Files collection is empty. The binary content is received as a normal form value (which only shows weird characters when parsed).
My understanding is that the clients' implementation is wrong. They are however mobile applications, so I have to stay backwards compatible. This is basically how the client is sending the file:
var client = new HttpClient();
var content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(File.ReadAllBytes("someimage.jpg")), "file");
await client.PutAsync("https://myapi/api/document", content);
The old ASP.NET Implementation parsed it like this (some parts removed):
var provider = new MultipartMemoryStreamProvider();
await request.Content.ReadAsMultipartAsync(provider);
Stream file = null;
foreach (var contentPart in provider.Contents)
{
if (partName.Equals("file", StringComparison.OrdinalIgnoreCase))
{
file = await contentPart.ReadAsStreamAsync();
}
}
In ASP.NET Core, file/form parsing is built in and MultipartMemoryStreamProvider no longer exists, so this is what I implemented:
public async Task<IActionResult> Put(IFormFileCollection files) // files is empty list
public async Task<IActionResult> Put(IFormFile file) // file is null
// ...
var formFile = request.Form.Files.GetFile("file");
// formFile is null
// requests.Form.Files is empty
Stream file = formFile.OpenReadStream();
The file can be retrieved via request.Form["file"], but its content is displayed as {����. No idea if I can get that back to my binary content.
I tried this code, but the file cannot be opened afterwards.
var fff = request.Form["file"];
using (var stream = System.IO.File.OpenWrite("out.jpg"))
using (StreamWriter sw = new StreamWriter(stream))
{
sw.Write(fff);
}
I was able to fix the issue by using MultipartReader instead of the old MultipartMemoryStreamProvider. The Microsoft docs and the corresponding GitHub sample helped a lot.
This is a stripped-down version of my final code. It is by no means a "general file upload" endpoint, see the example linked above if you need something that can handle more different scenarios.
[DisableFormValueModelBinding]
public async Task<IActionResult> Put(CancellationToken cancellationToken)
{
Stream fileStream = null;
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), new FormOptions().MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (contentDisposition.Name.Equals("file", StringComparison.OrdinalIgnoreCase))
{
fileStream = new MemoryStream();
await section.Body.CopyToAsync(fileStream);
fileStream.Position = 0;
}
}
section = await reader.ReadNextSectionAsync();
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}

Response object is null when using FeatureCollection on the DefaultHttpContext

I am testing some .net Core middleware and would like to run the middleware with the whole asp.net Core http pipeline instead of mocking it.
The problem is that somehow the Response object is not being set in the httpRequest when I use the Feature Collection and it is read only on the Request itself.
This code throws an exception when it tries to write to the Response Stream.
var fc = new FeatureCollection();
fc.Set<IHttpRequestFeature>(new HttpRequestFeature {
Headers = new HeaderDictionary { { "RandomHeaderName", "123" } }
});
var httpContext = new DefaultHttpContext(fc);
var middleware = new RequestValidationMiddleware(
next: async (innerHttpContext) =>
{
await innerHttpContext.Response.WriteAsync("test writing");
});
middleware.InvokeAsync(httpContext).GetAwaiter().GetResult();
By using a custom feature collection, you are excluding features that would have been added by the default constructor of the DefaultHttpContext
public DefaultHttpContext()
: this(new FeatureCollection())
{
Features.Set<IHttpRequestFeature>(new HttpRequestFeature());
Features.Set<IHttpResponseFeature>(new HttpResponseFeature());
Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
}
public DefaultHttpContext(IFeatureCollection features)
{
_features.Initalize(features);
_request = new DefaultHttpRequest(this);
_response = new DefaultHttpResponse(this);
}
try recreating what was done in the default constructor by also adding the required features needed to exercise your test
var fc = new FeatureCollection();
fc.Set<IHttpRequestFeature>(new HttpRequestFeature {
Headers = new HeaderDictionary { { "RandomHeaderName", "123" } }
});
//Add response features
fc.Set<IHttpResponseFeature>(new HttpResponseFeature());
var responseBodyStream = new MemoryStream();
fc.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(responseBodyStream ));
var httpContext = new DefaultHttpContext(fc);

Microsoft Graph API call hangs indefinitely

I am attempting to query Azure Active Directory User information using Microsoft Graph. I can authenticate fine but when I attempt to query user information client.Users my application hangs indefinitely: no timeout, no error, just hangs. I found this post however the suggestions there did not help me.
public bool GetUserByUniqueID(string uid, out GraphUser user)
{
bool ret = false;
user = new GraphUser();
if (Authenticate(out AuthToken token))
{
GraphServiceClient client = GetGraphServiceClient(token);
// The below code hangs indefinitely
User user = client.Users[uid].Request().Select(GraphProperties).GetAsync().GetAwaiter().GetResult();
if (user != null)
{
MapGraphUser(ret, user);
ret = true;
}
}
return ret;
}
private bool Authenticate(out AuthToken token)
{
bool ret = false;
token = new AuthToken();
string url = $"https://login.microsoftonline.com/{_tenant}/oauth2/v2.0/token";
RestClient client = new RestClient(url);
RestRequest request = new RestRequest(Method.POST);
request.Parameters.Add(new Parameter("grant_type", _grantType, ParameterType.GetOrPost));
request.Parameters.Add(new Parameter("scope", _scope, ParameterType.GetOrPost));
request.Parameters.Add(new Parameter("client_secret", _clientSecret, ParameterType.GetOrPost));
request.Parameters.Add(new Parameter("client_id", _clientId, ParameterType.GetOrPost));
IRestResponse response = client.Execute<AuthToken>(request);
if (response.StatusCode == HttpStatusCode.OK)
{
token = JsonConvert.DeserializeObject<AuthToken>(response.Content);
ret = true;
}
return ret;
}
Update 5/2/2019
Reverting Microsoft.Graph and Microsoft.Graph.Core to version 1.12 allows me to call .GetAwaiter().GetResult() within a synchronous context.
Update 11/18/2020
I have refactored my code to use async/await pattern with the latest version of Microsoft.Graph and Microsoft.Graph.Core.
public async Task<GraphUser> GetUserByUniqueID(string uid)
{
GraphUser ret = new GraphUser();
if (Authenticate(out AuthToken token))
{
GraphServiceClient client = GetGraphServiceClient(token);
User user = await client.Users[uid].Request().Select(GraphProperties).GetAsync();
if (user != null)
{
MapGraphUser(ret, user);
ret.Found = true;
}
}
return ret;
}
I was having the same issue. I found on another article somewhere that it had something to do with two task waiting to finish at once. i cant find that article now.
For me .GetAwaiter().GetResult(); was working within a scheduled job but not as a manual button press task.
as a result of playing around with it. What worked for me was replacing .GetAwaiter().GetResult() with await. (i'm not sure why this fixed it but it did)
From:
var results = graphServiceClient.Users[uid].Request().GetAsync().GetAwaiter().GetResult();
To:
var results = await graphServiceClient.Users[uid].Request().GetAsync();
Hope this helps someone in the future
I'm having the same issue with NPM package of the Graph API. Reverted to plain old request-promise. Now it's not stuck but does not always find the members of a group. Using beta version of the API works fine
I had the same issue when trying to get a list of sites, but I was using Microsoft.Graph V 4.47.0 and Microsoft.Graph.Core V 2.0.14, from within a MVC Web project. I was also using await.
var drives = await graphClient.Sites["root"].Lists
.Request()
.GetAsync();
The above just hangs. Changing to:
var drives = graphClient.Sites["root"].Lists
.Request()
.GetAsync()
.GetAwaiter()
.GetResult();
works as expected.
Full Code:
public async Task GetDrives(GraphServiceClient graphClient)
{
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");
O365Drives = new List<MyDriveInfo>();
var drives = graphClient.Sites["root"].Lists
.Request()
.GetAsync()
.GetAwaiter()
.GetResult();
foreach (var item in drives)
{
O365Drives.Add(new MyDriveInfo
{
Id = item.Id,
Name = item.Name,
WebUrl = item.WebUrl,
CreatedOn = item.CreatedDateTime,
ModifiedOn = item.LastModifiedDateTime
});
}
}
The above is called by firing an Ajax POST request when clicking on a button.
In another project, a console app, using Microsoft.Graph V 4.34.0 and Microsoft.Graph.Core V 2.0.9
var drives = await graphClient.Sites["root"].Lists
.Request()
.GetAsync();
Works as expected.
Full Code:
private static async Task GetDrives(GraphServiceClient graphClient)
{
AuthenticationConfig config = AuthenticationConfig.ReadFromJsonFile("appsettings.json");
myFileInfo.O365Drives = new List<MyDriveInfo>();
var drives = await graphClient.Sites[$"{config.SiteID}"].Lists
.Request()
.GetAsync();
foreach(var item in drives)
{
myFileInfo.O365Drives.Add(new MyDriveInfo
{
Id = item.Id,
Name = item.Name,
WebUrl = item.WebUrl,
CreatedOn = item.CreatedDateTime,
ModifiedOn = item.LastModifiedDateTime
});
}
}
The above is called by either running the console app manually or from a scheduled task.
I just thought I'd post my findings for the newer versions of Microsoft.Graph for anyone else having similar issues.

Authorization_RequestDenied when i have access token code

Blockquoteafter access token when I called graph API that returns Authorization_RequestDenied request for the access token
using (var webClient = new WebClient())
{
var requestParameters = new NameValueCollection();
requestParameters.Add("resource", resource);
requestParameters.Add("client_id", clientID);
requestParameters.Add("grant_type", "client_credentials");
requestParameters.Add("client_secret", secret);
var url = $"https://login.microsoftonline.com/{tenant}/oauth2/token";
var responsebytes = await webClient.UploadValuesTaskAsync(url,"POST",requestParameters);
var responsebody =Encoding.UTF8.GetString(responsebytes);
var obj = JsonConvert.DeserializeObject<JObject>(responsebody);
var token = obj["access_token"].Value<string>();
access_token = token;
}
after when i request form get the user list from Azure AD by this way
public async Task<List<listItems>> GetData1( string token)
{
HttpClient http = new HttpClient();
string query = "https://graph.microsoft.com/v1.0/users";
HttpRequestMessage httpClient = new HttpRequestMessage(HttpMethod.Get, query);
httpClient.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await http.SendAsync(httpClient);
var res1= await res.Content.ReadAsStringAsync();
List<listItems> lstUsers = new List<listItems>();
JObject results = JObject.Parse(res1); listItems itm;
foreach (var Jelem in results["value"])
{
string id = (string)Jelem["id"];
string displayName = (string)Jelem["displayName"];
itm = new listItems(); itm.id = id;
itm.displayname = displayName; lstUsers.Add(itm);
}
return lstUsers;
}
than i got "error": { "code": "Authorization_RequestDenied", "message": "Insufficient privileges to complete the operation.", "innerError": { "request-id": "1ba8a3e3-7e27-4bad-affd-6929b9af3a9f", "date": "2019-03-26T10:56:26" } the above error
please help me to solve this error
CAUSE
This problem occurs because the application does not have the required permission to access the user information. So you need to assign necessary privileged for this request.
SOLUTION
To access https://graph.microsoft.com/v1.0/users API One of the following permissions is required.
Permission type (from least to most privileged)
Delegated (work or school account) User.Read, User.ReadWrite,
User.ReadBasic.All,
User.Read.All, User.ReadWrite.All, Directory.Read.All,
Directory.ReadWrite.All,
Directory.AccessAsUser.All
Delegated (personal Microsoft account) User.Read, User.ReadWrite
Application User.Read.All, User.ReadWrite.All, Directory.Read.All,
Directory.ReadWrite.All
See the screen shot below:
AZURE PORTAL WAY OUT
To assign permission on azure portal see the screen shot below:
ASP.NET WEB FORM EXAMPLE:
1. Add New Aspx page To project
Take a new web form, here I have taken as Token.aspx and set its property like below
<%# Page Language="C#" AutoEventWireup="true" Async="true"
CodeBehind="Token.aspx.cs" Inherits="WebFormTest.Token" %>
2. Add New Reference from Nuget
In your project reference add a new service reference from nuget package manager console Like below:
3. Token.aspx.cs
Paste following code outside the scope of Page_Load method You might need to add following reference on your namespace once you encounter missing reference error.
using System.Net.Http;
using System.Net.Http.Headers;
class AccessToken
{
public string access_token { get; set; }
}
// Resource Owner Password Credentials Format
private async Task<string> GetTokenByROPCFormat()
{
string tokenUrl = $"https://login.microsoftonline.com/YourTenantId/oauth2/token";
var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
req.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "password",
["client_id"] = "ApplicationID",
["client_secret"] = "ApplicationSecret",
["resource"] = "https://graph.microsoft.com",
["username"] = "userEmailwithAccessPrivilege",
["password"] = "YourPassword"
});
dynamic json;
dynamic results;
HttpClient client = new HttpClient();
var res = await client.SendAsync(req);
json = await res.Content.ReadAsStringAsync();
//Token Output
results = JsonConvert.DeserializeObject<AccessToken>(json);
Console.WriteLine(results.access_token);
//New Block For Accessing Data from Microsoft Graph API
HttpClient newClient = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/me");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", results.access_token);
HttpResponseMessage response = await newClient.SendAsync(request);
string output = await response.Content.ReadAsStringAsync();
Console.WriteLine("Responsed data Is-\n\n" + output + "");
return output;
}
4. Call GetTokenByROPCFormat() Method inside Page_Load
Now call GetTokenByROPCFormat inside the Page_Load like below
RegisterAsyncTask(new PageAsyncTask(GetTokenByROPCFormat));
5. Token Output
If you set debugger on results variable you would get your token like below
6. Access Microsoft Graph API
Now move to following line and set your debugger like below
string output = await response.Content.ReadAsStringAsync();
You would see following output
Hope it would solve your problem. Thank you.

Categories