We have developed an application which calls Update Task Planner Graph API to update task in Planner App. The API was working fine until some recent change in the MS docs and now it keeps throwing below error.
A type named 'microsoft.taskServices.plannerCheckListItemCollection' could not be resolved by the model. When a model is available, each type name must resolve to a valid type.
Below is the code for creating and updating the Task using Graph API.
var newTask = new PlannerTask
{
PlanId = "planID",
BucketId = "bucketID",
Title = "title"
};
Logger.LogInfo(userLogs + "Task object created, calling Graph API");
var taskResponse = await graphClient.Planner.Tasks.Request().AddAsync(newTask);
PlannerTaskDetails taskDetails = new PlannerTaskDetails
{
Checklist = new PlannerChecklistItems { AdditionalData = checkListItems }
};
Logger.LogInfo(userLogs + "Getting Details of created Task");
PlannerTaskDetails getTaskDetails = await graphClient.Planner.Tasks[taskResponse.Id].Details.Request().GetAsync();
var eTagId = getTaskDetails.GetEtag();
Logger.LogInfo(userLogs + "Updating Task");
await graphClient.Planner.Tasks[taskResponse.Id].Details.Request()
.Header("Prefer", "return=representation")
.Header("If-Match", eTagId)
.UpdateAsync(taskDetails);
Code snippet for CheckListItems:
Dictionary<string, Object> checkListItems = new Dictionary<string, object>();
checkListItems.Add(key, new PlannerChecklistItem
{
Title = "title",
IsChecked = True
});
Also proper App Permissions in Azure are already given as this was working fine till last month.
This was a bug, and the issue should be resolved now.
Related
Context:
I am creating new MS Teams team using MS Graph API in C#
My code:
var newTeam = new Team()
{
DisplayName = model.DisplayName,
Description = model.Description,
AdditionalData = new Dictionary<string, object>
{
["template#odata.bind"] = $"{graph.BaseUrl}/teamsTemplates('standard')",
["members"] = owners.ToArray()
}
};
var team = await graph.Teams.Request().AddAsync(newTeam);
Problem:
The team is created fine but I can't get it's id. Return type of AddAsync method is Task<Team> but it always returns null.
I have checked server response using Fiddler and found out that the id of created team is returned in response headers.
Content-Location: /teams('cbf27e30-658b-4021-a8c6-4002b9adaf41')
Unfortunately I don't know how to access this information.
Luckily this is quite easy to achieve by using the requests base class BaseRequest and sending it yourself. With the received HttpResponseMessage you get the headers as documented here which contains the id of your new team.
The code also includes how to wait for the team creation to finish using a busy wait – which is not considered as best practice, but makes the sample easier. A better approach would be to store the team id and periodically query the creation status.
var newTeam = new Team()
{
DisplayName = model.DisplayName,
Description = model.Description,
AdditionalData = new Dictionary<string, object>
{
["template#odata.bind"] = $"{graph.BaseUrl}/teamsTemplates('standard')",
["members"] = owners.ToArray()
}
};
// we cannot use 'await client.Teams.Request().AddAsync(newTeam)'
// as we do NOT get the team ID back (object is always null) :(
BaseRequest request = (BaseRequest)graph.Teams.Request();
request.ContentType = "application/json";
request.Method = "POST";
string location;
using (HttpResponseMessage response = await request.SendRequestAsync(newTeam, CancellationToken.None))
location = response.Headers.Location.ToString();
// looks like: /teams('7070b1fd-1f14-4a06-8617-254724d63cde')/operations('c7c34e52-7ebf-4038-b306-f5af2d9891ac')
// but is documented as: /teams/7070b1fd-1f14-4a06-8617-254724d63cde/operations/c7c34e52-7ebf-4038-b306-f5af2d9891ac
// -> this split supports both of them
string[] locationParts = location.Split(new[] { '\'', '/', '(', ')' }, StringSplitOptions.RemoveEmptyEntries);
string teamId = locationParts[1];
string operationId = locationParts[3];
// before querying the first time we must wait some secs, else we get a 404
int delayInMilliseconds = 5_000;
while (true)
{
await Task.Delay(delayInMilliseconds);
// lets see how far the teams creation process is
TeamsAsyncOperation operation = await graph.Teams[teamId].Operations[operationId].Request().GetAsync();
if (operation.Status == TeamsAsyncOperationStatus.Succeeded)
break;
if (operation.Status == TeamsAsyncOperationStatus.Failed)
throw new Exception($"Failed to create team '{newTeam.DisplayName}': {operation.Error.Message} ({operation.Error.Code})");
// according to the docs, we should wait > 30 secs between calls
// https://learn.microsoft.com/en-us/graph/api/resources/teamsasyncoperation?view=graph-rest-1.0
delayInMilliseconds = 30_000;
}
// finally, do something with your team...
You will get your teamId by calling the GET joined Teams, it will get the team id ,name and description
I want to link the existing work items which are already created in a project under Azure DevOps by writing a code or program in C#, so is there any kind of API or SDK which can be used to link the work items programmatically?
The Workitems can be of any type i.e.
Bug
User Story
Issue
Task etc.
The linking between the Workitems can also be of any type i.e. Relational, Parent-Child, etc.
Recently I referred to this link for my problem.
The link contains issue very much related and similar to mine, however it is not working as expected when I tried it.
Given:
//int relatedId = ...
//int id = ...
//CancellationToken token = ...
//string organization = ...
//string projectName = ...
//WorkItemTrackingHttpClient azureClient = ...
Create a JsonPatchDocument as below:
JsonPatchDocument patchDoc = new JsonPatchDocument();
patchDoc.Add(
new JsonPatchOperation
{
From = null,
Operation = Operation.Add,
Path = "/relations/-",
Value = new {
rel = "System.LinkTypes.Related",
url = $"https://dev.azure.com/{organization}/{projectName}/_workitems/edit/{relatedId}",
attributes = new
{
comment = $"Created programmatically on {DateTime.Now}."
}
}
}
);
and call this async method of Azure DevOps SDK:
await azureClient.UpdateWorkItemAsync(patchDoc, id, false, true, true, WorkItemExpand.All, cancellationToken: token);
In the case above we created a related link using System.LinkTypes.Related referenceName.
For a full link types reference guide in Azure DevOps refer to this so's question or this microsoft's doc.
Below is error that I could not update refreshSchedule of the datasets:
{
"error": {
"code": "InvalidRequest",
"message": "Invalid NotifyOption value 'MailOnFailure' for app only owner requests"
}
}
Below is code to call it:
var datasets = await client.Datasets.GetDatasetsAsync(new Guid(_workspaceId));
var days = new List<Days?> { Days.Monday, Days.Tuesday, Days.Wednesday, Days.Thursday, Days.Friday, Days.Saturday, Days.Sunday };
var times = new List<string> { "00:00" };
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC");
var id = "XXX";
await client.Datasets.TakeOverAsync(new Guid(_workspaceId), id);
var refreshRequest = new RefreshRequest(NotifyOption.NoNotification);
// refresh datasets
await client.Datasets.RefreshDatasetAsync(new Guid(_workspaceId), id, refreshRequest);
// Target: Update RefreshSchedule (Exception for calling this)
await client.Datasets.UpdateRefreshScheduleInGroupAsync(new Guid(_workspaceId), id, refreshSchedule);
Can you pls let me know how the app is consuming the endpoint - User Interventaion or Completeley done through AppOnly or using the master user Credential?
Alternatively, if you don't want the MailOnfailure, can you set up explicitly No Notification for the Refresh Schedule and Try ?
just modified your piece of code and presented below :
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC", ScheduleNotifyOption.NoNotification);
The snippet :
var days = new List<Days?> { Days.Monday, Days.Tuesday, Days.Wednesday, Days.Thursday, Days.Friday, Days.Saturday, Days.Sunday };
var times = new List<string> { "00:00" };
//Fixed code
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC", ScheduleNotifyOption.NoNotification);
await client.Datasets.UpdateRefreshScheduleInGroupAsync(WorkspaceId, datasets.Id, refreshSchedule);
UPDATE
When i used ServicePrincipal (without any UserIntervention/Master User Credential)
I was able to repro your issue
Error
Status: BadRequest (400) Response: {"error":{"code":"InvalidRequest","message":"Invalid NotifyOption
value 'MailOnFailure' for app only owner requests"}}
I was able to get past the error by making use of the ScheduleNotifyOption.NoNotification in the refreshSchedule mentioned above.
Or if i use the app through cred of a mailenabled account.
Using the Azure DevOps REST API in C#, I'm creating a pull request and then attempting to complete it like this (simplified):
var pullRequest = new GitPullRequest {
Title = "My PR",
SourceRefName = "refs/heads/my",
TargetRefName = "refs/heads/master",
Commits = commits
};
pullRequest = await gitClient.CreatePullRequestAsync(pullRequest, repositoryId);
await Task.Delay(3000);
if (pullRequest.MergeStatus == PullRequestAsyncStatus.Succeeded) {
var pr2 = new GitPullRequest
{
LastMergeSourceCommit = pullRequest.LastMergeSourceCommit,
Status = PullRequestStatus.Completed
};
var result = await gitClient.UpdatePullRequestAsync(pullRequest, pullRequest.Repository.Id, pullRequest.pullRequestId);
}
This works fine if there's no conflicts. But if the pull request has conflicts, MergeStatus will be Conflicts. Now, let's assume someone resolves those conflicts manually and the PR is ready to be merged.
After resolving conflicts I get the pull request again
var pullRequest = await gitClient.GetPullRequestByIdAsync(pullRequestId);
pullRequest.MergeStatus is still Conflicts, even though UI is showing green.
Is there a way to refresh MergeStatus once it has been set to Conflicts? I tried updating the pull request by setting MergeStatus to Queued. Or is it a missing feature in the API?
Hopefully you got through this, so just in case someone else comes looking (like I did today) this worked for me.
var prOriginal = gitClient.CreatePullRequestAsync(
new GitPullRequest() {
SourceRefName = $"refs/heads/{Input.SrcBranch}",
TargetRefName = $"refs/heads/{Input.TgtBranch}",
Title = Input.Title,
Description = Input.Description
},
tgtRepo.Id).Result;
Thread.Sleep(TimeSpan.FromSeconds(30));
var statusRetry = 0;
var prTest = gitClient.GetPullRequestAsync(
tgtRepo.Id,
prOriginal.PullRequestId).Result;
while(PullRequestAsyncStatus.Succeeded != prTest.MergeStatus) {
// TODO decide when to quit.
Thread.Sleep(TimeSpan.FromSeconds(30));
prTest = gitClient.GetPullRequestAsync(
tgtRepo.Id,
prOriginal.PullRequestId).Result;
}
Debug.WriteLine($"MergeStatus: {prTest.MergeStatus}");
MergeStatus came back succeeded soon after I resolved the problems online. Now if I can get those conflicts resolved using the api I'll be in great shape.
I created a bot with a command, that allows the user to configure some sort of 'feed' to their channel.
This feed is supposed to send a message, save guild, channel and message id. And in a stand-alone update cycle, try to update the message with new information.
This all works fairly well, as long as it is within the same session.
Say the bot losses it's connection due to a discord outage, and re-connects x amount of time later, the bot no longer seems to be able to find, and thus update the message anymore.
In particular, it seems to be unable to retrieve the message by id
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
It's worth to note that I make use of _settings which is persisted in json format, and is loaded again upon bot reboot.
I also confirmed that the message still exists in the server at the channel, with the same message id. And that the bot has permissions to view the message history of the channel.
Thus my question, how come the GetMessageAsync is unable to retrieve a previously posted message after reconnecting?
Initialy invoked command
public async Task BindPlaytimeFeedAsync(ICommandContext context)
{
var builder = await _scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
var message = await context.Channel.SendMessageAsync(null, false, builder.Build());
_settings.PlaytimeFeed = new MessageInfo()
{
GuildId = context.Guild.Id,
ChannelId = context.Channel.Id,
MessageId = message.Id,
};
var ptFeedMessage = await context.Channel.SendMessageAsync("Playtime feed is now bound to this channel (this message self-destructs in 5 seconds)");
await Task.Delay(5000);
await ptFeedMessage.DeleteAsync();
}
The refresh interval of the feed is defined alongside the bot itself using a timer as seen below.
...
_client = new DiscordSocketClient(
new DiscordSocketConfig
{
LogLevel = LogSeverity.Verbose,
AlwaysDownloadUsers = true, // Start the cache off with updated information.
MessageCacheSize = 1000
}
);
_service = ConfigureServices();
_feedInterval = new Timer(async (e) =>
{
Console.WriteLine("doing feed stuff");
await HandleFeedsAsync();
}, null, 15000, 300000);
CmdHandler = new CommandHandler(_service, state);
...
private async Task HandleFeedsAsync()
{
var botSettings = _service.GetService<ISettings>() as BotSettings;
await HandleKdFeedAsync(botSettings.KdFeed);
await HandlePlaytimeFeedAsync(botSettings.PlaytimeFeed);
await HandleWeeklyPlaytimeFeed(botSettings.WeeklyPlaytimeFeed);
await HandleAdminFeed(botSettings);
}
And ultimately the message is overwritten using the below snippet.
private async Task HandlePlaytimeFeedAsync(MessageInfo playtimeFeed)
{
if (playtimeFeed == null)
return;
var scumService = _service.GetService<ScumService>();
var guild = _client.GetGuild(playtimeFeed.GuildId);
var channel = guild.GetTextChannel(playtimeFeed.ChannelId);
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
if (message == null)
return;
var builder = await scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
await message.ModifyAsync(prop =>
{
prop.Embed = builder.Build();
});
}
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
The GetMessageAsync method attempts to retrieve a message from cache as a SocketUserMessage, if however the message is not found in cache, a rest request is performed which would return a RestUserMessge. By performing a soft cast on the result of GetMessageAsync, you can get null if/when a RestUserMessage is returned.
When the possibility exists that the message you are dealing with can be either a Socket entity or Rest entity, simply use the interface to interact with it -- IUserMessage.