C# HTTP PATCH using HTTPClient - c#

I've written a test using Test Server in dot net core 3.1 and I'm trying to do a PATCH request to an endpoint. However as I'm new to using PATCH, I'm a bit stuck with how to send the correct object that the endpoint is expecting.
[Fact]
public async Task Patch()
{
var operations = new List<Operation>
{
new Operation("replace", "entryId", "'attendance ui", 5)
};
var jsonPatchDocument = new JsonPatchDocument(operations, new DefaultContractResolver());
// Act
var content = new StringContent(JsonConvert.SerializeObject(jsonPatchDocument), Encoding.UTF8, "application/json");
var httpResponse = await HttpClient.PatchAsync($"v1/Entry/1", content);
var actual = await httpResponse.Content.ReadAsStringAsync();
}
[HttpPatch("{entryId}")]
public async Task<ActionResult> Patch(int entryId, [FromBody] JsonPatchDocument<EntryModel> patchDocument)
{
if (patchDocument == null)
{
return BadRequest();
}
var existingEntry = _mapper.Map<EntryModel>(await _entryService.Get(entryId));
patchDocument.ApplyTo(existingEntry);
var entry = _mapper.Map<Entry>(existingEntry);
var updatedEntry = _mapper.Map<Entry>(await _entryService.Update(entryId, entry));
return Ok(await updatedEntry.ModelToPayload());
}
From the example I'm creating a JsonPatchDocument with a list of operations, serializing it to JSON and then doing PatchAsync with HTTP Client with the URL for the endpoint.
So my question is what is the shape of the object that I should be Patching and I'm doing this correctly in general?
I tried sending the EntryModel as shown in the picture below, however patchDocument.Operations has an empty list.
Thanks,
Nick

I ended up solving my problem by doing several things:
JsonPatchDocument doesn't seem to work without the dependency
services.AddControllers().AddNewtonsoftJson(); in Startup.cs. This is from the Nuget package `Microsoft.AspNetCore.Mvc.Newtonsoft.json.
There is an easier way to create the array than the answer from #Neil. Which is this: var patchDoc = new JsonPatchDocument<EntryModel>().Replace(o => o.EntryTypeId, 5);
You need this specific media type:
var content = new StringContent(JsonConvert.SerializeObject(patchDoc), Encoding.UTF8, "application/json-patch+json");
Here is the complete code:
/// <summary>
/// Verify PUT /Entrys is working and returns updated records
/// </summary>
[Fact]
public async Task Patch()
{
var patchDoc = new JsonPatchDocument<EntryModel>()
.Replace(o => o.EntryTypeId, 5);
var content = new StringContent(JsonConvert.SerializeObject(patchDoc), Encoding.UTF8, "application/json-patch+json");
var httpResponse = await HttpClient.PatchAsync($"v1/Entry/1", content);
var actual = await httpResponse.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.True(httpResponse.IsSuccessStatusCode);
}
/// <summary>
/// Endpoint to do partial update
/// </summary>
/// <returns></returns>
[HttpPatch("{entryId}")]
public async Task<ActionResult> Patch(int entryId, [FromBody] JsonPatchDocument<EntryModel> patchDocument)
{
if (patchDocument == null)
{
return BadRequest();
}
var existingEntry = _mapper.Map<EntryModel>(await _entryService.Get(entryId));
// Apply changes
patchDocument.ApplyTo(existingEntry);
var entry = _mapper.Map<Entry>(existingEntry);
var updatedEntry = _mapper.Map<Entry>(await _entryService.Update(entryId, entry));
return Ok();
}

Take a look at this page: https://learn.microsoft.com/en-us/aspnet/core/web-api/jsonpatch?view=aspnetcore-3.1
But the content is something like:
[ {
"op": "add",
"path": "/customerName",
"value": "Barry" }, {
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
} } ]

The JsonPatchDocument will not work. To make it work you have to add a media type formater.
Install Microsoft.AspNetCore.Mvc.NewtonsoftJson
In startup.cs , after AddControllers() add ->
.AddNewtonsoftJson(x =>
x.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver(); })
If you need to have JSON as the default media type formatter. Keep it before any other media type of formatter.

Related

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);

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

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

Task.Run sometimes returns twice

I have an azure function app that I call from a slack slash command.
Sometimes the function takes a little while to return the data requested, so I made that function return a "Calculating..." message to slack immediately, and run the actual processing on a Task.Run (the request contains a webhook that I post back to when I finally get the data) :
Task.Run(() => opsData.GenerateQuoteCheckMessage(incomingData, context.FunctionAppDirectory, log));
This works mostly fine, except every now and then when people are calling the function from slack, it will return the data twice. So it will show one "Calculating..." message and then 2 results returned from the above function.
BTW, Azure functions start with :
public static async Task
Thanks!
UPDATE : here is the code for the function:
[FunctionName("QuoteCheck")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequestMessage req, TraceWriter log, ExecutionContext context)
{
var opsHelper = new OpsHelper();
string bodyContent = await req.Content.ReadAsStringAsync();
var parsedBody = HttpUtility.ParseQueryString(bodyContent);
var commandName = parsedBody["command"];
var incomingBrandId = parsedBody["text"];
int.TryParse(incomingBrandId, out var brandId);
var responseUrl = parsedBody["response_url"];
var incomingData = new IncomingSlackRequestModel
{
UserName = parsedBody["user_name"],
ChannelName = parsedBody["channel_name"],
CommandName = commandName,
ResponseUri = new Uri(responseUrl),
BrandId = brandId
};
var opsData = OpsDataFactory.GetOpsData(context.FunctionAppDirectory, environment);
Task.Run(() => opsData.GenerateQuoteCheckMessage(incomingData, context.FunctionAppDirectory, log));
// Generate a "Calculating" response message based on the correct parameters being passed
var calculatingMessage = opsHelper.GenerateCalculatingMessage(incomingData);
// Return calculating message
return req.CreateResponse(HttpStatusCode.OK, calculatingMessage, JsonMediaTypeFormatter.DefaultMediaType);
}
}
And then the GenerateQuoteCheckMessage calculates some data and eventually posts back to slack (Using Rest Sharp) :
var client = new RestClient(responseUri);
var request = new RestRequest(Method.POST);
request.AddParameter("application/json; charset=utf-8", JsonConvert.SerializeObject(outgoingMessage), ParameterType.RequestBody);
client.Execute(request);
Using Kzrystof's suggestion, I added a service bus call in the function that posts to a queue, and added another function that reads off that queue and processes the request, responding to the webhook that slack gives me :
public void DeferProcessingToServiceBus(IncomingSlackRequestModel incomingSlackRequestModel)
{
var serializedModel = JsonConvert.SerializeObject(incomingSlackRequestModel);
var sbConnectionString = ConfigurationManager.AppSettings.Get("SERVICE_BUS_CONNECTION_STRING");
var sbQueueName = ConfigurationManager.AppSettings.Get("OpsNotificationsQueueName");
var client = QueueClient.CreateFromConnectionString(sbConnectionString, sbQueueName);
var brokeredMessage = new BrokeredMessage(serializedModel);
client.Send(brokeredMessage);
}

RestSharp asynchronous PUT

There is one application, the API with access to the database and one application that calls the API with RestSharp.
I implemented all async methods of RestSharp to work generic. So GET, POST, DELETE are all working.The only one I can't get to work is the PUT.
First of all this is my controllers PUT:
[HttpPut("{id}")]
public void Put(int id, [FromBody]ApplicationUser value)
{
string p = value.Email;
}
this is my method:
public Task<bool> PutRequestContentAsync<T>(string resource, object id, T resourceObject) where T : new()
{
RestClient client = new RestClient("http://localhost:54008/api/");
RestRequest request = new RestRequest($"{resource}/{{id}}", Method.PUT);
request.AddUrlSegment("id", id);
request.AddObject(resourceObject);
var tcs = new TaskCompletionSource<bool>();
var asyncHandler = client.ExecuteAsync<T>(request, r =>
{
tcs.SetResult(r.ResponseStatus == ResponseStatus.Completed);
});
return tcs.Task;
}
and this is my call in a view (all other calls of GET,... are working fine):
bool putOk = await new RepositoryCall()
.PutRequestContentAsync("Values", 2,
new ApplicationUser {
Email="test#xxxxxxx.de"
}
);
with debugging, the response-status is Completed but the PUT is never called.
Any idea what the problem could be?
So finally I got my answer myself... (sit yesterday 6 hours and no result, today one more hour and it works)
public Task<bool> PutRequestContentAsync<T>(string resource, object id, T resourceObject) where T : new()
{
RestClient client = new RestClient("http://localhost:54008/api/");
RestRequest request = new RestRequest($"{resource}/{{id}}", Method.PUT);
request.AddUrlSegment("id", id);
request.RequestFormat = DataFormat.Json;
request.AddBody(resourceObject);
var tcs = new TaskCompletionSource<bool>();
var asyncHandler = client.ExecuteAsync<T>(request, (response) => {
tcs.SetResult(response.ResponseStatus == ResponseStatus.Completed);
});
return tcs.Task;
}
the trick was to add a RequestFormat and changing AddObject to AddBody :)

Error "405 Method Not Allow" When Calling Put method in Postman with body parameter

I was trying to call the Put method through Postman and always getting error: "405 Method Not Allow" and "Message": "The requested resource does not support http method 'PUT'."
I'm using DocumentDB and C#. Here is my code:
[Route("multilanguage/Resources/{id}/{Language}")]
[HttpPut]
public async Task<IHttpActionResult> UpdateResource(string Id, string Language, string text)
{
client = new DocumentClient(new Uri(EndPoint), AuthKey);
var collectionLink = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId);
var query = new SqlQuerySpec("SELECT * FROM MultiLanguage as m where m.id = #pmId",
new SqlParameterCollection(new SqlParameter[] { new SqlParameter { Name = "#pmId", Value = Id } }));
Document doc = client.CreateDocumentQuery<Document>(
collectionLink, query).AsEnumerable().FirstOrDefault();
List<Models.Translations> d = doc.GetPropertyValue<List<Models.Translations>>("Translations");
Models.Translations temp = d.Find(p => p.Language == Language);
temp.Content = text;
temp.LastModified = DateTimeOffset.Now;
temp.ModifiedBy = "admin";
doc.SetPropertyValue("Translations", d);
Document updated = await client.ReplaceDocumentAsync(doc);
return Ok();
}
When I call the Put method throught Postman, I call "http://localhost:XXXX/multilanguage/resources/2/En". "2" and "En" are the first two parameters in my code. And I also specify the "text" parameter value in the Postman request Body with x-www-form-urlencoded type: key = text, value = Test! This put method suppose to update the temp.Content value to "Test!". However, it always failed with the error I mentioned above.
Did I miss anything here?
The 405 error when performing a PUT request to web api is a well known topic. You can find many solutions in this or this SO question.
And for the design of you controller:
PUT are designed to have a body, just like POST and in your case
you should send all parameters in the body instead.
You should create a class which contains the objects you want to send to the server:
public class resourceClass
{
public string Id { get; set; }
public string Language { get; set; }
public string text { get; set; }
}
Then specify the route without the attribute routing and get the object from the request body
[Route("multilanguage/Resources/PutResource")]
[HttpPut]
public async Task<IHttpActionResult> UpdateResource([FromBody] resourceClass obj)
{
client = new DocumentClient(new Uri(EndPoint), AuthKey);
var collectionLink = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId);
var query = new SqlQuerySpec("SELECT * FROM MultiLanguage as m where m.id = #pmId",
new SqlParameterCollection(new SqlParameter[] { new SqlParameter { Name = "#pmId", Value = Id } }));
Document doc = client.CreateDocumentQuery<Document>(
collectionLink, query).AsEnumerable().FirstOrDefault();
List<Models.Translations> d = doc.GetPropertyValue<List<Models.Translations>>("Translations");
Models.Translations temp = d.Find(p => p.Language == Language);
temp.Content = text;
temp.LastModified = DateTimeOffset.Now;
temp.ModifiedBy = "admin";
doc.SetPropertyValue("Translations", d);
Document updated = await client.ReplaceDocumentAsync(doc);
return Ok();
}
From the client you could add an object to the PUT request of Content-Type application/json like this
var data = {
Id: clientId,
Language: clientLanguage,
text: clientText
};
Don't forget to stringify the json when adding it to the http request
data: JSON.stringify(data),
The PUT controller will then be reached at "http://localhost:XXXX/multilanguage/resources/putresource".
Check the URL for which you are posting the data, in my case the URL was incorrect because of which I got these errors, also verify that in Body you should select raw and change the Text to JSON if you are passing a JSON as a data.

Categories