While messing with dictionaries to create form data for FormUrlEncodedContent so I can send data to server using HTTP client, I started thinking about easier and cleaner way to do it. I was writing integration tests and I wanted to re-use some model classes instead of dictionaries. Here’s how to do it. Sample of integration test is incluced.
Those who are currently writing integration tests for some ASP.NET Core web application may also find the following writings interesting:
Problem
I write integration tests for ASP.NET Core web application. There are tests that use HTTP POST to send some data to server. I can create POST body like shown here.
var formDictionary = new Dictionary<string, string>();
formDictionary.Add("Id", "1");
formDictionary.Add("Title", "Item title");
var formContent = new FormUrlEncodedContent(formDictionary);
var response = await client.PostAsync(url, formContent);
I don’t like this code and I would use it only if other options turn out less optimal. Instead, I want something like shown here.
var folder = new MediaFolder { Id = 1, Title = "Folder title" };
var formBody = GetFormBodyOfObject(folder);
// Act
var response = await client.PostAsync(url, fromBody);
Or why not something more elegant like code here.
var folder = new MediaFolder { Id = 1, Title = "Folder title" };
// Act
var response = await client.PostAsync(url, folder.ToFormBody());
This way I can use model classes from web application because these classes are used on forms anyway to move data between browser and server. If otherwise breaking change is introduced then compiler will tell me about it.
Serializing objects to Dictionary<string,string>
Thanks to Arnaud Auroux from Geek Learning we have ready-made solution to take. ToKeyValue() method by Arnaud turns object to JSON and then uses Newtonsoft.JSON library convert object to dictionary like FormUrlEncodedContent class wants.
ToKeyValue() method is the main work horse for my solution. I turned it to extension method and made serializer avoid cyclic references.
public static IDictionary<string, string> ToKeyValue(this object metaToken)
{
if (metaToken == null)
{
return null;
}
// Added by me: avoid cyclic references
var serializer = new JsonSerializer { ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var token = metaToken as JToken;
if (token == null)
{
// Modified by me: use serializer defined above
return ToKeyValue(JObject.FromObject(metaToken, serializer));
}
if (token.HasValues)
{
var contentData = new Dictionary<string, string>();
foreach (var child in token.Children().ToList())
{
var childContent = child.ToKeyValue();
if (childContent != null)
{
contentData = contentData.Concat(childContent)
.ToDictionary(k => k.Key, v => v.Value);
}
}
return contentData;
}
var jValue = token as JValue;
if (jValue?.Value == null)
{
return null;
}
var value = jValue?.Type == JTokenType.Date ?
jValue?.ToString("o", CultureInfo.InvariantCulture) :
jValue?.ToString(CultureInfo.InvariantCulture);
return new Dictionary<string, string> { { token.Path, value } };
}
Now we can serialize our objects to Dictionary<string,string> and it also works with object hierarchies.
Serializing object to form data
But we are not there yet. ToKeyValue() extension method is useful if we want to modify dictionary before it goes to FormUrlEncodedContent. For cases when we don’t need any modifications to dictionary I wrote ToFormData() extension method.
public static FormUrlEncodedContent ToFormData(this object obj)
{
var formData = obj.ToKeyValue();
return new FormUrlEncodedContent(formData);
}
Using these extension methods I can write integration tests like shown here.
[Fact]
public async Task CreateFolder_CreatesFolderWithValidData()
{
// Arrange
var factory = GetFactory(hasUser: true);
var client = factory.CreateClient();
var url = "Home/CreateFolder";
var folder = new MediaFolder { Id = 1, Title = "Folder title" };
// Act
var response = await client.PostAsync(url, folder.ToFormData());
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
}
Wrapping up
We started with annoying problem and ended up with pretty nice solution. Instead of manually filling dictionaries used for HTTP POST requests in integration tests we found nice JSON based solution by Arnaud Auroux and we built ToKeyValue() and ToFormData() extension methods to simplify object serialization to .NET Core HttpClient form data. These extension methods are general enough to have them in some common library for our projects.
View Comments (6)
Am I missing something here? Why the additional dependency on json.net to do this where simply using reflection to get all object properties and their values would've been enough?
Hi I have a case when I have to post a class like below using form
public class AddTenantRequestdto
{
public IFormFile TenantLogo { get; set; }
public string TenantTitle { get; set; }
public List ApplicationName { get; set; }
}
Then how would i use FormUrlEncodedContent. Can u please suggest
Hi,
please be more specific. From where data is coming and to where it is going? What's the role of AddTenantRequestDto?
@gunnar, I have an API name CreateTenant, In which I want to upload Image, AddTenantRequestdto is its request parameter. Below is my API Action Method and AddTenantRequestdto.
public class AddTenantRequestdto
{
public IFormFile TenantLogo { get; set; }
public string TenantTitle { get; set; }
public List ApplicationName { get; set; }
}
This is my API
public async Task CreateTenant(string tenantId, string applicationId, [FromForm]AddTenantRequestdto addTenantRequest)
{
}
I want to call this api in my Integration Test case, I am able to successfully done that, but I having issue for list parameter name ApplicationName. Here is a details what I did for IT test.
public async Task Tenant_Create_Success(AddTenantRequestdto addTenantRequest)
{
HttpClient Client = new HttpClient();
var formDictionary = new Dictionary();
formDictionary.Add("EnableOTP", JsonConvert.SerializeObject(addTenantRequest.EnableOTP));
formDictionary.Add("ApplicationName", JsonConvert.SerializeObject(addTenantRequest.ApplicationName));
formDictionary.Add("TenantLogo", JsonConvert.SerializeObject(addTenantRequest.TenantLogo));
var formContent = new FormUrlEncodedContent(formDictionary);
var response = await Client.PostAsync("http://localhost:61234/Tenants/CreateTenant", formContent);
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
ApplicationName is a List collection of string.
works awesome, thanks