I'm using Flurl in integration testing and trying to configure a client to log responses (using Flurl.Http 3.0.0).
I'm using event handlers to read the response as a string and then log it.
However, if the calling code uses IFlurlResponse.GetJsonAsync<> when the logging is enabled, the deserialized object is null (I imagine because the stream has already been read).
I thought it may be possible because I can see that internally Flurl tracks whether the response stream has been read (using the _streamRead and _capturedBody members).
Here's a repro, using Flurl.Http 3.0.0:
class Program
{
static async Task Main(string[] args)
{
using (var client = new FlurlClient("https://jsonplaceholder.typicode.com/"))
{
var post = await client
.Request("posts/1")
.ConfigureRequest(settings => settings.AfterCallAsync = LogResponse)
.GetJsonAsync<Post>();
Console.WriteLine($"Is null with logging enabled: {post is null}"); // prints True
post = await client.Request("posts/1").GetJsonAsync<Post>();
Console.WriteLine($"Is null with logging disabled: {post is null}"); // prints False
}
}
private static async Task LogResponse(FlurlCall call)
{
var responseString = await call.Response.GetStringAsync();
Console.WriteLine(responseString);
}
}
public class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}
Output:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Is null with logging enabled: True
Is null with logging disabled: False
If I change the calling code from using GetJsonAsync<> to GetStringAsync and handle json deserialization myself then the response can be 'read' twice, but it's more verbose.
UPDATE: The fix has been released.
You're correct that in most cases Flurl "captures" deserialized responses so they can easily be read multiple times. But, as I think you came close to concluding, this currently only works when you're deserializing to the same type. If both the event handler and main logic were deserializing to a string, or both to a Post, this should work.
I'm not sure if I'd quite call this a bug (maybe?), but I'm definitely convinced that getting it to work as you're expecting here would be a nice enhancement and not too hard. Please open an issue and I'll look at getting this done in a future release.
Related
I'm new to Blazor and bUnit. I have component that renders an edit form and I get the values for the form in my OnInitializedAsync event.
I'm having trouble working out how to use cut.WaitForState() or cut.WaitForAssertion().
Here's my razor code:
#page "/{AppId:guid}/app-settings-edit"
<section class="app-settings-edit">
<h1 class="page-title">Application Settings</h1>
#if (InitializedComplete)
{
<p>Hello World</p>
...
And my code behind:
public partial class AppSettingsEdit
{
protected bool InitializedComplete;
[Parameter]
public Guid AppId { get; set; }
[ValidateComplexType]
public AppSettings AppSettings { get; set; } = new AppSettings();
[Inject]
public IAppSettingsDataService AppSettingsDataService { get; set; }
protected override async Task OnInitializedAsync()
{
AppSettings = await AppSettingsDataService.Get(AppId);
InitializedComplete = true;
}
...
And here's my Test:
[Fact]
public void MyFact()
{
Services.AddSingleton<IAppSettingsDataService, MockAppSettingsDataService>(x => new MockAppSettingsDataService(x.GetRequiredService<HttpClient>()));
var cut = RenderComponent<AppSettingsEdit>(parameters => parameters
.Add(p => p.AppId, Guid.Parse("55E5097B-B56A-40D7-8A02-A5B94AAAD6E1"))
);
Assert.NotNull(cut.Instance.AppSettingsDataService);
cut.WaitForState(() => cut.Find("p").TextContent == "Hello World", new TimeSpan(0, 0, 5));
cut.MarkupMatches("<p>Hello World</p>");
}
When I debug the test, I can see the OnInitializedAsync firing, however my markup never changes to include 'Hello World' and the WaitForState() command fails.
Are you certain that the task returned from your AppSettingsDataService.Get() method ever completes?
I would make sure that the task returned from AppSettingsDataService.Get() is already completed, otherwise you need to a way to complete the task after the component is rendered. There are many ways to do this, it all depends on how your mock is implemented.
As for your WaitFor, you can just use the WaitForAssertion method in this case, i.e.: cut.WaitForAssertion(() => cut.MarkupMatches("<p>Hello World</p>"));
A little background:
The WaitFor* methods are used when the component under test is being rendered asynchronously, since the test, running in a different thread, doesn't know when that will happen.
In general, you should never need to set a custom timeout, the default is 1 second, but the WaitFor* methods will retry the assertion/predicate every time a renderer happens. Its only when the thing that triggers the rendering will take more than one second, e.g. if you are using bUnit to perform end-2-end testing and e.g. pulling data from a real web service.
I am using .netcore to create a simple REST API and have used the swashbuckle package (swashbuckle.aspnetcore\1.1.0) so that I can devtest it easily.
ConfigureServices:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Meetings Calculator API",
Version = "v1",
Description = "An API to provide pricing information related to journeys",
});
});
Configure
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
});
All is well except for a post call I have developed. It appears that the dictionary I have defined to be passed in the body is not served over to the service correctly.
Method Signature
[HttpPost("CalculateCost", Name = "CalculateCost")]
public IActionResult getJourneyCalculation([FromBody] IDictionary<String, String> locations)
Generated json config
"parameters":[
{
"name":"locations",
"in":"body",
"required":false,
"schema":{
"type":"object",
"additionalProperties":{
"type":"string"
}
}
}
],
I think that is the point where the dictionary is supposed to be so I think the config side of this is not working correctly. Does anyone know what I need to do to get this to pick up the IDictionary correctly?
I came across a post that discussed this which stated that there was a bug (or at least inconsitency) with the spelling causing problems "s" v "S" in String but I have tried both with no change. I think this was also old and quoted as fixed. I also tried a few suggestions altering the UseSwaggerUI endpoint but all that did for me was to leave me with a blank screen.
[UPDATE]
I seem to be getting just one entry from the dictionary so for instance if I use the intput:
{
"2":"London",
"2":"Bristol",
"2":"London",
"2":"Newcastle"
}
the last entry will make it to the controller.
[UPDATE]
I have tried it with and it will work intermittently:
with arguments ([""]) - ModelState is Invalid
with arguments (["System.Collections.Generic.Dictionary`2[System.String,System.Int32]"]) - ModelState is Valid
with the same inputs:
{
"London":2,
"Bristol:2",
"London:2",
"Newcastle":2
}
Intermittent is a bit of a claim as it worked twice and not since.
Ok I have found my way around this by defining my own class to hold the two values instead.
public class FromLocation
{
[Required]
public string Location { get; set; }
[Required]
public int Attendees { get; set; }
}
Changed the controllers signature to:
public IActionResult getJourneyCalculation([FromBody] IList<FromLocation> locations)
now the yellow schema box lists the type and I can send the information over as required. Not sure what was going on with the dictionary but life is too short.
I am back with more questions. I am using Unity 5.5.1f1 and Watson Developer Cloud Unity SDK v0.13.0. And instead of using widgets, I am using the scripts inside of Watson/Examples/ServiceExamples/Scripts, they worked out pretty well, and I was able to get the conversation service going. However, I soon realized that with this setup, I can't seem to reach to child nodes. Please look at the conversation editor screenshot below:
If I test this conversation online, it will turn out like this:
Watson: Hello, welcome to the paradise!
Me: Can you turn off the music please.
Watson: Ok, let's turn off something, you can say music, ac, and lights.
Me: music
Watson: ok, turn the music off. [child node]
However, when I do this through Unity, it becomes:
Watson: Hello, welcome to the paradise!
Me: Can you turn off the music please.
Watson: Ok, let's turn off something, you can say music, ac, and lights.
Me: music
Watson: say what? [anything_else node]
It seems the conversation only stays at the parent nodes, and does not reach the child node at all. Or maybe every message to the server resets the service? Please help!!
best,
In order to reach the child nodes you need to pass the context in the request. In the response there will be a context property you can pass to the next request.
Conversation m_Conversation = new Conversation();
private void SendInitalMessage(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentNullException("input");
// Send inital message to the service
m_Conversation.Message(OnInitalMessage, <workspace-id>, input);
}
private void OnInitalMessage(MessageResponse resp, string customData)
{
if (resp != null)
{
// Check response here
// Create a message request object with the context
MessageRequest messageRequest = new MessageRequest();
messageRequest.InputText = <input-text>;
messageRequest.alternate_intents = true;
messageRequest.ContextData = resp.context; // Context of the conversation
// Send the second message
SendFollowupMessage(messageRequest);
}
else
{
Debug.Log("Message Only: Failed to invoke Message();");
}
}
private void SendFollowupMessage(MessageRequest messageRequest)
{
if (messageRequest == null)
throw new ArgumentNullException("messageRequest");
m_Conversation.Message(OnFollowupMessage, <workspace-id>, messageRequest);
}
private void OnFollowupMessage(MessageResponse resp, string customData)
{
if (resp != null)
{
// Check response here
}
else
{
Debug.Log("Full Request: Failed to invoke Message();");
}
}
The context object contains the conversationID and other data for the service to keep track of where in the conversation the user is.
"context": {
"conversation_id": "<conversation-id>",
"system": {
"dialog_stack": [
{
"dialog_node": "<dialog-node>"
}
],
"dialog_turn_counter": <turn-counter>,
"dialog_request_counter": <request-counter>,
"branch_exited": <branch-exited>,
"branch_exited_reason": "<exited-reason>"
},
"defaultCounter": <default-counter>
}
EDIT: The data model seems to have updated. resp.context.system.dialog_stack should not be an array of strings. It should be an array of RuntimeDialogStack objects.
[fsObject]
SystemResponse
{
public RuntimeDialogStack[] dialog_stack {get;set;}
public int dialog_turn_counter {get;set;}
public int dialog_request_counter {get;set;}
}
[fsObject]
public class RuntimeDialogStack
{
public string dialog_node {get;set;}
public bool invoked_subdialog {get;set;}
}
EDIT 2: Looks like I've been testing with mismatching version strings. Please try this data model and ensure the version param is 2017-02-03 in the VisualRecognition data model.
[fsObject]
SystemResponse
{
public DialogNode[] dialog_stack {get;set;}
public int dialog_turn_counter {get;set;}
public int dialog_request_counter {get;set;}
}
[fsObject]
DialogNode
{
public string dialog_node {get;set;}
public bool invoked_subdialog {get;set;}
}
I'm having a problem with Web API 2 (.net 4.5.1) in that it seems to ignore PATCH requests where the property is an integer, but processes other types without a problem (I've tested string and decimal).
I’ve setup an unsecured test API with a 'products' controller at http://playapi.azurewebsites.net/api/products. If you do a GET to that URL, you’ll get something like this product back:
{"Id": 1,"Name": "Xbox One","Category": "gaming","Price": 300,"Stock": 5}
‘Name’ and ‘Category’ are both strings, ‘Price’ is a Decimal and ‘Stock’ is an Integer.
If you send these requests, they both work (You’ll get a 200/OK with the updated entity):
PATCH, http://playapi.azurewebsites.net/api/products/1 with {"Price": 600.00}
PATCH, http://playapi.azurewebsites.net/api/products/1 with
{"Category": "Electronics"}
However, if you send this, it returns 200/OK, but does not make the update and the stock remains at the original value
PATCH, http://playapi.azurewebsites.net/api/products/1 with
{"Stock": 4}
My controller code is fairly standard boiler plate code (from the scaffolded ODATA controller but moved into a standard API controller):
// PATCH: api/Products/5
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> PatchOrder(int id, Delta<Product> patch)
{
Validate(patch.GetEntity());
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var item = await db.Products.FindAsync(id);
if (item == null)
{
return NotFound();
}
patch.Patch(item);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return Ok(item);
}
My model for 'Product' is as follows:
namespace PlayAPI.Models
{
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public double Price { get; set; }
public int Stock { get; set; }
}
}
When I debug the controller, I see that the ‘patch’ object has a _changedProperties collection which has no items in it when I do an integer request, but when I do any other kind of request it has the key that I changed.
Should web API support PATCH requests for integer properties? If so, do I need to do anything special on the server or client to make it work?
As a quick fix, Change the int to an Int64 on PlayAPI.Models.Product.
public Int64 Stock { get; set; }
It's my understanding that The Delta object used to patch the existing object doesn’t use JSON.net to convert and is silently throwing an Invalid cast exception when it parses JSON and then compares to the existing object from your database. You can read more about the bug over here: http://aspnetwebstack.codeplex.com/workitem/777
If you can't actually change the data type successfully, there may be a decent hack fix that you can use. Just attached unreadable data into the query string.
Here's a function you can call from within your Patch functions. As long as you aren't using the query string parameters specifically named what it's looking for, you should be just fine.
/// <summary>
/// Tries to attach additional parameters from the query string onto the delta object.
/// This uses the parameters extraInt32 and extraInt16, which can be used multiple times.
/// The parameter format is "PropertyName|Integer"
/// <para>Example: ?extraInt32=Prop1|123&extraInt16=Prop2|88&extraInt32=Prop3|null</para>
/// </summary>
[NonAction]
protected void SetAdditionalPatchIntegers<TEntity>(Delta<TEntity> deltaEntity, bool allowNull = true)
{
var queryParameters = Request.GetQueryNameValuePairs();
foreach (var param in queryParameters.Where(pair =>
pair.Key == "extraInt32" ||
pair.Key == "extraInt16"))
{
if (param.Value.Count(v => v == '|') != 1)
continue;
var splitParam = param.Value.Split('|');
if (allowNull &&
(String.IsNullOrWhiteSpace(splitParam[1]) ||
splitParam[1].Equals("null", StringComparison.OrdinalIgnoreCase)))
{
deltaEntity.TrySetPropertyValue(splitParam[0], null);
continue;
}
if (param.Key == "extraInt32")
{
int extraInt;
if (Int32.TryParse(splitParam[1], out extraInt))
{
deltaEntity.TrySetPropertyValue(splitParam[0], extraInt);
}
}
if (param.Key == "extraInt16")
{
short extraShort;
if (Int16.TryParse(splitParam[1], out extraShort))
{
deltaEntity.TrySetPropertyValue(splitParam[0], extraShort);
}
}
}
}
I really hate that there isn't a better answer, but at least something can be done about it.
I'm working with MVC 4 Web API and I have this dummy ValueProvider:
DummyValueProvider.cs
class DummyValueProvider : IValueProvider
{
public DummyValueProvider()
{
}
public bool ContainsPrefix(string prefix)
{
return true;
}
public ValueProviderResult GetValue(string key)
{
return new ValueProviderResult("testing", "testing", System.Globalization.CultureInfo.InvariantCulture);
}
}
class DummyValueProviderFactory : System.Web.Http.ValueProviders.ValueProviderFactory
{
public override IValueProvider GetValueProvider(System.Web.Http.Controllers.HttpActionContext actionContext)
{
return new DummyValueProvider();
}
}
This ValueProvider should return true for any key asked, so it will always supply a value to the model binder when it needs. The ValueProvider is registered in the WebApiConfig like this:
WebApiConfig.cs
config.Services.Add(typeof(ValueProviderFactory), new DummyValueProviderFactory());
The code compiles and runs fine.
I also have this action in the Account API controller:
AccountController.cs
public HttpResponseMessage Register(string foo) { ... }
The action gets called fine when I call it like below:
/register?foo=bar
And foo is filled with bar as expected; but if I call:
/register
The server returns 404 with the message No HTTP resource was found that matches the request URI 'http://localhost:14459/register'.
Also, I put breakpoints inside methods ContainsPrefix() and GetValue(), but they never get triggered.
What am I doing wrong? Shouldn't DummyValueProvider be providing the value testing to parameter foo?
Try this
public HttpResponseMessage Get([ValueProvider(typeof(DummyValueProviderFactory))] string foo) {... }
I higly suggest you to read this recent article to customize Web Api Binding.
Update:
After reading the article the OP was able to discover the solution. It was that using the parameter attribute [ModelBinder] was required for it to work. This was because unless the parameter is annotated, [FromUri] is assumed. Once annotated with [ModelBinder] the registered handlers are executed.