Using ASP.NET Core 2.0 for 1st time.
I have a web project which references a DLL that i have created.
In this DLL is a simple method...
namespace InformedWorkerApi
{
[Route("api/[controller]")]
public class RegistrationController: Controller
{
private readonly IAccountRepository _accountRepository;
public RegistrationController(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
[HttpPost()]
[Route("SignOn")]
public async Task<InformedWorkerModels.Account> SignOn([FromBody]SignOnRequest model)
{
return await _accountRepository.SignOn(model.EmailAddress, model.Password);
}
}
}
I have also created a test project which references my DLL...
[TestMethod]
public async Task SignOn()
{
var webHostBuilder = new WebHostBuilder()
.UseStartup<Startup>();
using (var host = new TestServer(webHostBuilder))
{
using (var client = host.CreateClient())
{
var requestData = new SignOnRequest { EmailAddress = "emailAddress", Password= "password" };
var content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.PostAsync("api/Registration/SignOn", content);
//do some asserts here
}
}
}
I get the error status code 404 not found.
What am i getting so wrong here please?
You have email and password as part of the route but send them as content in the body. that wont match the route template for the action and thus 404 Not Found.
Create a model to hold the data
public class SignOnRequest {
[Required]
public string emailAddress { get; set; }
[Required]
public string password { get; set; }
}
Also for core you have to specify with parameter attributes where the framework should look for parameters.
[Route("api/[controller]")]
public class RegistrationController : Controller{
[HttpPost()]
[Route("SignOn")] // Matches POST api/Registration/SignOn
public async Task<IActionResult> SignOn([FromBody]SignOnRequest model) {
if(ModelState.IsValid) {
var response = await _accountRepository.SignOn(model.emailAddress, model.password);
return Ok(response);
}
return BadRequest();
}
}
Which should now match the request being made in the integration test
var requestData = new { emailAddress = "emailAddress", password = "password" };
var content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.PostAsync("api/Registration/SignOn", content);
As you also mentioned that the controller is in another assembly you would need to make sure that the controller is registered with the ServiceCollection within Startup so that the framework is aware of how to resolve the controller. Update the ConfigureServices to include
services.AddMvc()
.AddApplicationPart(typeof(RegistrationController).GetTypeInfo().Assembly)
.AddControllersAsServices();
as referenced from the following answer ASP.NET Core MVC controllers in separate assembly
Related
I am trying to make a post request from WPF to Web API using the following code but the request parameter is always null.
Request Model
public class Document
{
public string FileName { get; set; }
public byte[] Buffer { get; set; }
}
public class Request
{
public string Uploader { get; set; }
public List<Document> Documents { get; set; }
}
WPF Client
var obj = new Request()
{
Uploader = "John Doe",
Documents = new List<Document>
{
new Document()
{
FileName ="I Love Coding.pdf",
Buffer = System.IO.File.ReadAllBytes(#"C:\Users\john.doe\Downloads\I Love Coding.pdf.pdf")
}
}
};
using (var http = new HttpClient())
{
var encodedJson = JsonConvert.SerializeObject(obj);
var conent = new StringContent(encodedJson, Encoding.UTF8, "application/json");
HttpResponseMessage response = await http.PostAsync("https://my-app.com/api/upload", conent);
response.EnsureSuccessStatusCode();
}
Web API
[Route("")]
public class AppController : ControllerBase
{
[HttpPost]
[Route("api/upload")]
public async Task<IActionResult> UploadDocumentsAsync([FromBody] Request request)
{
// request is always null when app is running in production
// https://my-app.com/api/upload
//request is not null when running on https://localhost:8080/api/upload
}
}
Please what am I missing in the above implementation?
The request parameter is not null on localhost but always null in production.
Please what am I missing in the above implementation? The request
parameter is not null on localhost but always null in production.
Well, not sure how are getting data on local server becuse, you are sending MultipartFormData means your POCO object and file buffer. As you may know we can send json object in FromBody but not the files as json. Thus, I am not sure how it working in local and getting null data is logical in IIS Or Azure.
what am I missing in the above implementation?
As explained above, for sending both POCO object and Files as byte or steam we need to use FromForm and beside that, we need to bind our request object as MultipartFormDataContent to resolve your null data on your UploadDocumentsAsync API action.
Required Change For Solution:
WPF:
In your WPF http request please update your request code snippet as following:
var obj = new Request()
{
Uploader = "John Doe",
Documents = new List<Document>
{
new Document()
{
FileName ="I Love Coding.pdf",
Buffer = System.IO.File.ReadAllBytes(#"YourFilePath")
}
}
};
var httpClient = new HttpClient
{
BaseAddress = new("https://YourServerURL")
};
var formContent = new MultipartFormDataContent();
formContent.Add(new StringContent(obj.Uploader), "Uploader");
formContent.Add(new StringContent(obj.Documents[0].FileName), "Documents[0].FileName");
formContent.Add(new StreamContent(new MemoryStream(obj.Documents[0].Buffer)), "Documents[0].Buffer", obj.Documents[0].FileName);
var response = await httpClient.PostAsync("/api/upload", formContent);
if (response.IsSuccessStatusCode)
{
var responseFromAzureIIS = await response.Content.ReadAsStringAsync();
}
Note: Class in WPF side would remain same as before. No changes required.
Asp.net Core Web API:
In asp.net core web API side you should use [FromForm] instead of [FromBody]
So your controller Action would as following:
[Route("")]
public class AppController : ControllerBase
{
[HttpPost]
[Route("api/upload")]
public async Task<IActionResult> UploadDocumentsAsync([FromForm] Request file)
{
if (file.Documents[0].Buffer == null)
{
return Ok("Null File");
}
return Ok("File Received");
}
}
Note: For remote debugging I have checked the logs and for double check I have used a simple conditionals whether file.Documents[0].Buffer == null. I have tested both in local, IIS and Azure and working accordingly.
Update POCO Class in API Project:
For buffer you have used byte for your WPF project but for Web API project update that to IFormFile instead of byte. It should be as following:
public class Document
{
public string FileName { get; set; }
public IFormFile Buffer { get; set; }
}
public class Request
{
public string Uploader { get; set; }
public List<Document> Documents { get; set; }
}
Output:
If you would like to know more details on it you could check our official document here
I need to save the changes I make in my model through API call in my database. I have checked my API is working fine when I am running it individually on Web. But its giving me an error StatusCode: 405, ReasonPhrase: 'Method Not Allowed'. I am trying to send and object and trying to see whether the request made was completed or not. When I am trying to debug it, it is not sending hit on my API controller.
Here is my model class:
public class Customer
{
[Required]
public Guid CustomerId { get; set; }
public int Age { get; set; }
public int Phone { get; set; }
}
PUT Method in API:
[HttpPut]
[Route("api/[controller]/{customer}")]
public IActionResult EditCustomer(Customer customer)
{
var cust = _customerData.EditCustomer(customer);
if (cust == string.Empty)
{
return Ok();
}
else
{
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
}
The method I am using in project to call API:
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(apiBaseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
);
var sum = await client.PutAsJsonAsync("api/Customer/", customer);
if (sum.StatusCode == System.Net.HttpStatusCode.OK)
{
return RedirectToActionPermanent(actionName: "SingIn");
}
else
{
TempData["msg"] = "There is an error";
return View();
}
where baseaddress= {https://localhost:44398/}
EditCustomer Method
public string EditCustomer(Customer customer)
{
try
{
var pro = _customerContext.Customer.Where(e => e.CustomerId == customer.CustomerId).FirstOrDefault();
pro.Age = customer.Age;
pro.Phone = customer.Phone;
pro.Name = customer.Name;
_customerContext.Entry(pro).State = EntityState.Modified;
_customerContext.SaveChanges();
}
catch(Exception e)
{
return e.Message;
}
return string.Empty;
}
You need to fix your action route by removing {Customer}, since you send customer in request body, not as a route value
[Route("~/api/Customer")]
and request
var sum = await client.PutAsJsonAsync("/api/Customer", customer);
or better fix the acttion route name to meaningfull
[Route("~/api/EditCustomer")]
and
var sum = await client.PutAsJsonAsync("/api/EditCustomer", customer);
AsJsonAsync sometimes causes problems
try this code
var json = JsonSerializer.Serialize(customer);
//or if you are using Newtonsoft
var json = JsonConvert.SerializeObject(customer);
var contentData = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PutAsync("/api/Customer", contentData);
if (response.IsSuccessStatusCode)
return RedirectToActionPermanent("SingIn");
else
{
TempData["msg"] = "There is an error";
return View();
}
but IMHO I would prefer to use
client.PostAsync("/api/EditCustomer", contentData);
instead of Put.
and added [FromBody] to action
[HttpPost("~/api/EditCustomer")]
public IActionResult EditCustomer([FromBody] Customer customer)
I am no pro in web APIs but I suspect it could be due to the fact that the API expects customer to be in request URL.
Try and change the API route to [Route("api/[controller]")]
This could've been a comment but I don't have enough reputation :)
This is an example of an Action method inside HomeController:
[HttpPost]
public async Task<dynamic> UnitTest(string data)
{
var httpClient = new HttpClient();
var request = JsonConvert.SerializeObject(data);
var url = "https://jsonplaceholder.typicode.com/posts";
var response = await httpClient.PostAsync(url, new StringContent(request, Encoding.UTF8, "application/json"));
string responseContent = await response.Content.ReadAsStringAsync();
return responseContent;
}
I want to test it, but I do not know how. I tried the following:
[TestMethod]
public async Task JsonRightTest()
{
MyModelR model1 = new MyModelR
{
Title = "foo",
Body = "bar",
UserId = 1
};
string output1 = JsonConvert.SerializeObject(model1);
var url = "Home/UnitTest";
var response = await _client.PostAsync(url, new StringContent(output1, Encoding.UTF8, "application/json"));
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);
// Assert
Assert.AreEqual(1,
responseModel.UserId);
}
internal class MyModel
{
public string Title { get; set; }
public string Body { get; set; }
public int UserId { get; set; }
public int Id { get; set; }
}
internal class MyModelR
{
public string Title { get; set; }
public string Body { get; set; }
public int UserId { get; set; }
}
Unfortunately, the above does not work. Since I am very confused could you give me some answers to the following:
What's the best way to test UnitTest action? Is my approach wrong? Do I just have to call the API from JsonRightTest method and not involve the action?
Actually, in that case do we have a unit or integrated test?
I want to call the actual external end point.
The API (https://jsonplaceholder.typicode.com/posts) is found on the Internet and is available for testing purposes.
This appears to be an XY problem and a mixing of concerns.
The code under test is tightly coupled to implementation concerns and should encapsulate that external call behind a service abstraction that can be mocked during isolated unit tests.
Some refactoring steps that should be followed....
Those models being constructed in the test should be in the action.
[HttpPost]
public async Task<IActionResult> UnitTest([FromBody]MyDataR data) {
var httpClient = new HttpClient();
var requestJson = JsonConvert.SerializeObject(data);
var url = "https://jsonplaceholder.typicode.com/posts";
var response = await httpClient.PostAsync(url, new StringContent(requestJson, Encoding.UTF8, "application/json"));
if(response.IsSuccessStatusCode) {
var responseContent = await response.Content.ReadAsStringAsync();
var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);
return Ok(responseModel);
}else
return StatusCode(response.StatusCode);
}
Refactoring further, the actual calling of the external endpoint should be abstracted away
public interface IExternalService {
Task<MyModel> PostDataAsync(MyData data);
}
and implemented accordingly
public class ExternalService : IExternalService {
// should consider abstracting this as well but that is another matter
static Lazy<HttpClient> _httpClient = new Lazy<HttpClient>(() => new HttpClient());
private HttpClient httpClient {
get { return _httpClient.Value; }
}
public async Task<MyModel> PostDataAsync(MyData data) {
var requestJson = JsonConvert.SerializeObject(data);
var url = "https://jsonplaceholder.typicode.com/posts";
var content = new StringContent(requestJson, Encoding.UTF8, "application/json")
var response = await httpClient.PostAsync(url, content);
var responseContent = await response.Content.ReadAsStringAsync();
if(response.IsSuccessStatusCode) {
var responseContent = await response.Content.ReadAsStringAsync();
var responseModel = JsonConvert.DeserializeObject<MyModel>(responseContent);
return responseModel;
}else
return null;
}
}
with the action in the the controller now looking like
private readonly IExternalService externalService; // Assumed injected into the controller
[HttpPost]
public async Task<IActionResult> UnitTest([FromBody]MyDataR data) {
var responseModel = await externalService.PostDataAsync(data);
if(responseModel != null) {
return Ok(responseModel);
}else
return BadRequest();
}
By removing the tight coupling to the external service call , this would allow the controller to be tested in isolation as needed to verify that it behaves as expected.
The external service call implementation can now be tested on its own if the desire is to check that the external end point behaves as expected. This would be considered an integration test due to its reliance on the actual external endpoint.
[TestMethod]
public async Task JsonRightTest() {
// Arrange
var expected = 1;
var model = new MyModelR {
Title = "foo",
Body = "bar",
UserId = 1
};
var target = new ExternalService(); // System under test
// Act
var responseModel = await target.PostDataAsync(model);
// Assert
Assert.IsNotNull(responseModel);
var actual = responseModel.UserId;
Assert.AreEqual(expected, actual);
}
This should now allow for easier inspection of the external service to verify that it behaves as expected.
In production you would make sure that the external service abstraction and its implementation are registered in the composition root.
services.AddTransient<IExternalService, ExternalService>();
so that it is injected into the dependent controller correctly.
I have an ApiController and would like to test it with unit tests including the routing.
An example:
[RoutePrefix("prefix")]
public class Controller : ApiController
{
[HttpGet]
[Route("{id1}")]
public int Add(int id1, [FromUri] int id2)
{
return id1 + id2;
}
}
I would now like to test this method. I see, that I can test it like an ordinary method. But I would also like to test it with the translation of the URL to the method parameters.
Basically I would like to have an automatic test where I call a URL like prefix/10?id2=5 and get a result of 15. Is this somehow possible?
I wrote a little helper class for in-memory integration testing that can be called as part of the test suit.
internal interface IHttpTestServer : IDisposable {
HttpConfiguration Configuration { get; }
HttpClient CreateClient();
}
internal class HttpTestServer : IHttpTestServer {
HttpServer httpServer;
public HttpTestServer(HttpConfiguration configuration = null) {
httpServer = new HttpServer(configuration ?? new HttpConfiguration());
}
public HttpConfiguration Configuration {
get { return httpServer.Configuration; }
}
public HttpClient CreateClient() {
var client = new HttpClient(httpServer);
return client;
}
public void Dispose() {
if (httpServer != null) {
httpServer.Dispose();
httpServer = null;
}
}
public static IHttpTestServer Create(HttpConfiguration configuration = null) {
return new HttpTestServer(configuration);
}
}
And would then use it like this
[TestMethod]
public async Task HttpClient_Should_Get_OKStatus_From_InMemory_Hosting() {
using (var server = new HttpTestServer()) {
MyWebAPiProjectNamespace.WebApiConfig.Configure(server.Configuration);
var client = server.CreateClient();
string url = "http://localhost/prefix/10?id2=5";
var expected = 15;
var request = new HttpRequestMessage {
RequestUri = new Uri(url),
Method = HttpMethod.Get
};
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using (var response = await client.SendAsync(request)) {
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadAsAsync<int>();
Assert.AreEqual(expected, result);
}
}
}
This will configure an in-memory test server that the test can make calls to using its httpclient. It is in essence an end-to-end integration test.
Create an OWIN StartUp class using Microsoft ASP.NET Web API 2.2 OWIN package:
public class Startup
{
public void Configuration(IAppBuilder builder)
{
var config = new HttpConfiguration();
builder.UseWebApi(config);
config.MapHttpAttributeRoutes();
config.EnsureInitialized();
}
}
Use Microsoft ASP.NET Web API 2.2 Self Host package in your tests (used NUnit for example):
[Test]
[TestCase(10, 5, 15)]
[TestCase(1, 2, 3)]
// add your test cases
public async Task AdditionTests(int a, int b, int result)
{
// Arrange
var address = "http://localhost:5050";
using (WebApp.Start<Startup>(address))
{
var client = new HttpClient();
var requestUri = $"{address}/prefix/{a}?id2={b}";
// Act
var response = await client.GetAsync(requestUri);
// Assert
Assert.IsTrue(await response.Content.ReadAsAsync<int>() == result);
}
}
That is an integration test, not a unit test. If you wanted to automate this you would have to have a tool that would launch/host your web api and then execute requests against it.
If you wanted to keep it as a unit test though you could validate the attributes on the class and the method and check the values.
var type = typeof(Controller);
var attributeRoutePrefix = type.GetCustomAttribute(typeof(RoutePrefixAttribute)) as RoutePrefixAttribute;
Assert.IsNotNull(attributeRoutePrefix);
Assert.AreEqual("prefix", attributeRoutePrefix.Prefix);
var methodAttribute = type.GetMethod(nameof(Controller.Add)).GetCustomAttribute(typeof(RouteAttribute)) as RouteAttribute;
Assert.IsNotNull(methodAttribute);
Assert.AreEqual("id1", methodAttribute.Template);
Its possible by using postman or fiddler to test with url param..
When I curl to the /test route it works fine, however the test below 404's when trying to hit the in memory server on the same route.
When inspecting _client and _config appear to be ok - although I am not sure how to confirm that my in memory server is functioning correctly.
Does anybody know how I can get my in memory web server to map it's routes correctly so my test method can reach it?
namespace Robo.Tests.Controllers
{
[TestClass]
public class IntegrationTests
{
private HttpMessageInvoker _client;
private HttpConfiguration _config = new HttpConfiguration();
[TestInitialize]
public void SetupTest()
{
_config.MapHttpAttributeRoutes();
_config.EnsureInitialized();
var server = new HttpServer(_config);
_client = new HttpMessageInvoker(server);
}
[TestMethod]
public async Task Test()
{
var result = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://localhost/test"), CancellationToken.None);
}
}
}
and controller in case you are interested
namespace Robo.Controllers
{
//[ValidationActionFilter]
public class CVController : ApiController
{
[HttpGet]
[Route("test")]
public async Task<IHttpActionResult> test()
{
return Ok();
}
}
}
For in-memory server testing The following utility class was created. It basically wraps the setup functionality in the example shown.
internal interface IHttpTestServer : IDisposable {
HttpConfiguration Configuration { get; }
HttpClient CreateClient();
}
internal class HttpTestServer : IHttpTestServer {
HttpServer httpServer;
public HttpTestServer(HttpConfiguration configuration = null) {
httpServer = new HttpServer(configuration ?? new HttpConfiguration());
}
public HttpConfiguration Configuration {
get { return httpServer.Configuration; }
}
public HttpClient CreateClient() {
var client = new HttpClient(httpServer);
return client;
}
public void Dispose() {
if (httpServer != null) {
httpServer.Dispose();
httpServer = null;
}
}
public static IHttpTestServer Create(HttpConfiguration configuration = null) {
return new HttpTestServer(configuration);
}
}
The following test was crafted to demonstrate the use of in memory server using OP
[TestClass]
public class IntegrationTests {
[TestMethod]
public async Task Test() {
using (var server = HttpTestServer.Create()) {
//Arrange
var config = server.Configuration;
config.MapHttpAttributeRoutes();
config.EnsureInitialized();
var client = server.CreateClient();
var url = "http://localhost/test";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var expected = System.Net.HttpStatusCode.OK;
//Act
var result = await client.SendAsync(request, CancellationToken.None);
//Assert
Assert.IsNotNull(result);
Assert.AreEqual(expected, result.StatusCode);
}
}
public class CVController : ApiController {
[HttpGet]
[Route("test")]
public async Task<IHttpActionResult> test() {
return Ok();
}
}
}
Test passes.
The thing about this example is that the test and controller exist in same assembly so map attribute scans the assembly it was called in and found API controller with attribute routes. If controller lived in another project then the web API config of that project should be called on the HttpConfiguration to properly configure web API.
UPDATE
The test project and web api project should be two separate projects. That said, The web project should have a WebApiConfig.cs file with a static WebApiConfig.Register class and method. That method takes a HttpConfiguration parameter. The test should use that method to configure the api for in memory calls.
[TestClass]
public class IntegrationTests {
[TestMethod]
public async Task Test() {
using (var server = HttpTestServer.Create()) {
//Arrange
var config = server.Configuration;
//Config server
MyWebApiNamespace.WebApiConfig.Register(config);
var client = server.CreateClient();
var url = "http://localhost/test";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var expected = System.Net.HttpStatusCode.OK;
//Act
var result = await client.SendAsync(request, CancellationToken.None);
//Assert
Assert.IsNotNull(result);
Assert.AreEqual(expected, result.StatusCode);
}
}
}