I am having an issue with the following code, now I understand what the problem is but I don't have a solution to make the code do what I want.
// POST: api/Airports
[HttpPost]
public async Task<ActionResult<Airport>> CreateAirport(AirportCreateDto airportCreateDto)
{
var airportModel = _mapper.Map<Airport>(airportCreateDto);
_repository.CreateAirport(airportModel);
await _repository.SaveChanges();
var airportReadDto = _mapper.Map<AirportReadDto>(airportModel);
return CreatedAtRoute(nameof(GetAirport), new { airportReadDto.ID }, airportReadDto);
}
The CreatedAtRoute() method is the issue. Now, GetAirport is of type Task<ActionResult> which is probably the cause of the problem.
Here's the error
System.InvalidOperationException: No route matches the supplied values.
at Microsoft.AspNetCore.Mvc.CreatedAtRouteResult.OnFormatting(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext
context, ObjectResult result, Type objectType, Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context,
ObjectResult result)
at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultAsync(IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State&
next, Scope& scope, Object& state, Boolean& isCompleted)
at
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,
TFilterAsync
]()
Is there a solution where I can keep using CreatedAtRoute() and GetAirport of type Task<ActionResult>?
Here's the code for GetAirport
// GET: api/Airports/5
[HttpGet("{id}")]
public async Task<ActionResult<Airport>> GetAirport(int id)
{
var airport = await _repository.GetAirportById(id);
if (airport == null)
{
return NotFound();
}
return Ok(_mapper.Map<AirportReadDto>(airport.Value));
}
System.InvalidOperationException: No route matches the supplied values.
You can try to explicitly set route name as below, which works for me.
[HttpGet("{id}", Name = "GetAirport")]
public async Task<ActionResult> GetAirport()
{
//...
Action method CreateAirport
[HttpPost]
public async Task<ActionResult<Airport>> CreateAirport(AirportCreateDto airportCreateDto)
{
//...
return CreatedAtRoute(nameof(GetAirport), new { id = airportReadDto.ID }, airportReadDto);
}
Test Result
Related
I´m trying to create a custom model binder that cancels the request when the given data is invalid.
public sealed class DeploymentIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
string modelName = bindingContext.ModelName;
string? value = bindingContext
.ValueProvider
.GetValue(modelName)
.FirstValue;
if (value is null)
return Task.CompletedTask;
if(DeploymentId.TryParse(value, out var id))
{
bindingContext.Result = ModelBindingResult.Success(id);
}
else
{
bindingContext.ModelState.TryAddModelError(modelName, $"{value} is not a valid {nameof(DeploymentId)}.");
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
[HttpGet]
[Route("download/{deploymentId}")]
public async Task<IActionResult> DownloadDeployment(DeploymentId deploymentId)
{
...
}
I expected to not hit the endpoint if I pass in an invalid DeploymentId.
But instead the method gets called with deploymentId = null.
Do I have a wrong expectation/Do I missunderstand how the binders should work?
And if, how would I do it the wright way?
Thanks for your answers! 🙏
You can refer to this document. And use url like: https://localhost:7273/authors/index?id=1
result:
I have a need to use a SeeOther HTTP response code for one of my API routes.
The ControllerBase class has no SeeOther method so I have made an extension method for it, trying to follow the same pattern as the AcceptedAtAction method, passing in a string for action and controller name and an object of route values, but my UrlHelper.Action method keeps returning null when trying to build the action URL for my "Location" header.
I know I must have missed something simple but I just cant spot it, any thoughts?
Here are some (simplified) snippets of code my code
Extension method
public static class ControllerBaseExtensions
{
public static ActionResult SeeOther(this ControllerBase controller, string actionName, string controllerName, object routeValues)
{
var url = controller.Url.Action(
actionName,
controllerName,
new RouteValueDictionary(routeValues),
controller.Request.Scheme,
controller.Request.Host.ToUriComponent());
controller.Response.Headers.Add("Location", url);
return new StatusCodeResult(303);
}
}
Controller route
[ApiController]
[Route("/{id:guid}/forms")]
public class FormsController : ControllerBase
{
[Authorize(Policy = "AuthPolicy")]
[HttpPost]
public async Task<IActionResult> Post([FromRoute] Guid caseId, [FromBody] PostFormResourceRepresentation body, CancellationToken cancellationToken)
{
if (body.Type != FormType.A) return BadRequest("Unsupported form type provided");
var requestId = Guid.NewGuid();
return this.SeeOther("Pending", "Forms", new { requestId });
}
[Authorize(Policy = "AuthPolicy")]
[HttpGet("pending/{requestId:guid}")]
public IActionResult Pending([FromRoute] Guid caseId, [FromRoute] Guid requestId, CancellationToken cancellationToken)
{
return Ok();
}
}
Okay I worked it out, was something very simple.
I forgot to pass one of the route parameters into my Extension when I was calling it.
[Route("/{id:guid}/forms")]
public class FormsController : ControllerBase
{
[Authorize(Policy = "AuthPolicy")]
[HttpPost]
public async Task<IActionResult> Post([FromRoute] Guid caseId, [FromBody] PostFormResourceRepresentation body, CancellationToken cancellationToken)
{
if (body.Type != FormType.A) return BadRequest("Unsupported form type provided");
var requestId = Guid.NewGuid();
return this.SeeOther("Pending", "Forms", new { requestId, id // this was missing });
}
}
I have an idToken string that is returned by google-sign-in use in mobile app with flutter and firebase:
Code in flutter
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:google_sign_in/google_sign_in.dart';
class Authentication {
static GoogleSignIn googleSignIn;
static Future<User> signInWithGoogle(BuildContext context) async {
FirebaseAuth auth = FirebaseAuth.instance;
User user;
googleSignIn = GoogleSignIn();
final GoogleSignInAccount googleSignInAccount = await googleSignIn.signIn();
if (googleSignInAccount != null) {
final GoogleSignInAuthentication googleSignInAuthentication =
await googleSignInAccount.authentication;
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleSignInAuthentication.accessToken,
idToken: googleSignInAuthentication.idToken,
);
try {
final UserCredential userCredential =
await auth.signInWithCredential(credential);
user = userCredential.user;
} on FirebaseAuthException catch (e) {
if (e.code == 'account-exists-with-different-credential') {
// handle the error here
} else if (e.code == 'invalid-credential') {
// handle the error here
}
} catch (e) {
// handle the error here
}
print("Credential token: ${credential.token}");
print("Credential provider id: ${credential.providerId}");
print("AccessToken: ${googleSignInAuthentication.accessToken}");
print("ID Token: ${googleSignInAuthentication.idToken}");
print("AccessToken.length: ${googleSignInAuthentication.accessToken.length}");
print("IdToken.length: ${googleSignInAuthentication.idToken.length}");
}
return user;
}
static Future signOut() async {
await googleSignIn.signOut();
FirebaseAuth.instance.signOut();
}
}
ID token is returned.
And I use this code to decode it:
public JwtPayload PayloadInfo(string idToken)
{
var jwtToken = new JwtSecurityToken(idToken);
JwtPayload payload = jwtToken.Payload;
return payload;
}
It works fine in the console app but with the .net 5 web API it fail with error:
System.ArgumentException: IDX12739: JWT: 'System.String' has three segments but is not in proper JWS format.
I research that my idToken is not in JWS type... and I don't know how to solve this.
Weirdly, the function PayloadInfo works fine in C# console app but in the web API, it doesn't.
Controller
[HttpPost("login-google")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GoogleLoginAsync(
[FromBody] ExternalAuthModel model)
{
if (ModelState.IsValid)
{
var result = await
_userService.GoogleExternalLoginAsync(model);
if (result.IsSuccess)
{
return Ok(result);
}
return BadRequest(result);
}
return BadRequest("Somethings going wrong...");
}
ExternalAuthModel
public class ExternalAuthModel
{
public string Provider { get; set; }
public string IdToken { get; set; }
}
GoogleExternalLoginAsync function in my Service
public async Task<UserManagerResponse> GoogleExternalLoginAsync(ExternalAuthModel model)
{
var payload = _jwtHandler.PayloadInfo(model.IdToken);
if (payload is null)
{
return new UserManagerResponse
{
Message = "Invalid google authentication.",
IsSuccess = false
};
}
var info = new UserLoginInfo(model.Provider, payload.Sub, model.Provider);
var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
if (user is null)
{
user = await _userManager.FindByEmailAsync(payload["email"].ToString());
await _userManager.CreateAsync(user);
if (!await _roleManager.RoleExistsAsync(UserRoles.User))
await _roleManager.CreateAsync(new IdentityRole(UserRoles.User));
await _userManager.AddToRoleAsync(user, UserRoles.User);
await _userManager.AddLoginAsync(user, info);
}
else
{
await _userManager.AddLoginAsync(user, info);
}
if (user is null)
{
return new UserManagerResponse
{
Message = "Invalid google authentication.",
IsSuccess = false
};
}
var token = await _jwtHandler.GenerateToken(user);
return new UserManagerResponse
{
Message = token[0],
IsSuccess = true,
ExpireDate = DateTime.Parse(token[1])
};
}
PayloadInfo function
public JwtPayload PayloadInfo(string idToken)
{
// Exception when excute this line... it says my idToken is not in
// JWS compact format....
var jwtToken = new JwtSecurityToken(idToken);
JwtPayload payload = jwtToken.Payload;
return payload;
}
Log...
2021-07-04T16:10:30.3810628+07:00 [ERR] An unhandled exception has occurred while executing the request. (48a46595)
System.ArgumentException: IDX12739: JWT: 'System.String' has three segments but is not in proper JWS format.
at System.IdentityModel.Tokens.Jwt.JwtSecurityToken..ctor(String jwtEncodedString)
at FTask.AuthServices.Helpers.JwtHandler.PayloadInfo(String idToken) in D:\cn7\Project\hao\ftask\FTask.AuthServices\Helpers\JwtHandler.cs:line 83
at FTask.AuthServices.Services.UserService.GoogleExternalLoginAsync(ExternalAuthModel model) in D:\cn7\Project\hao\ftask\FTask.AuthServices\Services\UserService.cs:line 163
at FTask.Api.Controllers.AuthController.GoogleLoginAsync(ExternalAuthModel model) in D:\cn7\Project\hao\ftask\FTask.Api\Controllers\AuthController.cs:line 109
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Please test this:
public JwtPayload GetPayload()
{
var token = "<your token>";
var handler = new JwtSecurityTokenHandler();
var tokenData = handler.ReadJwtToken(token);
return tokenData.Payload;
}
i tested your token in .Net 5 Web API and got this result:
this error occurred and I tries all the possible solutions on all sites and still not working
I tries swaggeroperation
I tries Route
I tries HttpGet("List/{id}")
nothing worked for me
Error details are:
An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Ambiguous HTTP method for action - MandobX.API.Controllers.ShipmentOperationsController.GetShipmentOperations (MandobX.API). Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
My code:
controllers Actions:
//Get lists of drivers, regions and packagges type to create shipment operation
[Route("Init")]
[HttpGet]
public async Task<IActionResult> InitShipment()
{
ShipmentViewModel shipmentInitViewModel = new ShipmentViewModel
{
Drivers = await _context.Drivers.Include(d => d.User).Include(d => d.Vehicle).ToListAsync(),
PackageTypes = await _context.PackageTypes.ToListAsync(),
Regions = await _context.Regions.ToListAsync()
};
return Ok(new Response { Code = "200", Data = shipmentInitViewModel, Msg = "Task Completed Succesfully", Status = "1" });
}
// GET: api/ShipmentOperations
[SwaggerOperation("List")]
[Route("List")]
[HttpGet("List/{userId}")]
public async Task<ActionResult<IEnumerable<ShipmentOperation>>> GetShipmentOperations()
{
return await _context.ShipmentOperations.ToListAsync();
}
[SwaggerOperation("Details")]
[Route("Details")]
[HttpGet("Details/{id}")]
public async Task<ActionResult<ShipmentOperation>> GetShipmentOperation(string id)
{
var shipmentOperation = await _context.ShipmentOperations.FindAsync(id);
if (shipmentOperation == null)
{
return NotFound();
}
return shipmentOperation;
}
Can anyone help me with that?
the answer was on github on this post
https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/982
the solution is making the route annotation responsible for the generated route like this:
[Route("List/{userId}")]
[HttpGet]
public async Task<ActionResult<IEnumerable<ShipmentOperation>>>
GetShipmentOperations(string userId)
{
return await _context.ShipmentOperations.ToListAsync();
}
// GET: api/ShipmentOperations/5
[Route("Details/{id}")]
[HttpGet]
public async Task<ActionResult<ShipmentOperation>> GetShipmentOperation(string id)
{
var shipmentOperation = await _context.ShipmentOperations.FindAsync(id);
if (shipmentOperation == null)
{
return NotFound();
}
return shipmentOperation;
}
Hello I need to test my APIcontroller which implements ApiController and I don't know how to do this, I know basics of UnitTesting but this is a little bit too complex for me. Also I don't know how to use automapper in unit tests, so maybe someone could help me with this
Here is my controller:
namespace Vidly.Controllers.Api
{
public class CustomersController : ApiController
{
private ApplicationDbContext _context;
public CustomersController(ApplicationDbContext _context)
{
_context = new ApplicationDbContext();
}
// GET /api/customers
public IHttpActionResult GetCustomers(string query = null)
{
var customersQuery = _context.Customers
.Include(c => c.MembershipType);
if (!String.IsNullOrWhiteSpace(query))
customersQuery = customersQuery.Where(c => c.Name.Contains(query));
var customerDtos = customersQuery
.ToList()
.Select(Mapper.Map<Customer, CustomerDto>);
return Ok(customerDtos);
}
// GET /api/customers/1
public IHttpActionResult GetCustomer(int id)
{
var customer = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customer == null)
return NotFound();
return Ok(Mapper.Map<Customer, CustomerDto>(customer));
}
// POST /api/customers
[HttpPost]
public IHttpActionResult CreateCustomer(CustomerDto customerDto)
{
if (!ModelState.IsValid)
return BadRequest();
var customer = Mapper.Map<CustomerDto, Customer>(customerDto);
_context.Customers.Add(customer);
_context.SaveChanges();
customerDto.Id = customer.Id;
return Created(new Uri(Request.RequestUri + "/" + customer.Id), customerDto);
}
// PUT /api/customers/1
[HttpPut]
public IHttpActionResult UpdateCustomer(int id, CustomerDto customerDto)
{
if (!ModelState.IsValid)
return BadRequest();
var customerInDb = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customerInDb == null)
return NotFound();
Mapper.Map(customerDto, customerInDb);
_context.SaveChanges();
return Ok();
}
// DELETE /api/customers/1
[HttpDelete]
public IHttpActionResult DeleteCustomer(int id)
{
var customerInDb = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customerInDb == null)
return NotFound();
_context.Customers.Remove(customerInDb);
_context.SaveChanges();
return Ok();
}
}
}
So how do I need to start, do I need some kind of interface like this to mock dbcontext:
public interface IAPICustomerRepository
{
IHttpActionResult GetCustomers(string query = null);
IHttpActionResult GetCustomer(int id);
IHttpActionResult CreateCustomer(CustomerDto customerDto);
IHttpActionResult UpdateCustomer(int id, CustomerDto customerDto);
IHttpActionResult DeleteCustomer(int id);
}
Or maybe I can write Unit tests without mocking.
UPDATE
After I edited my code with Nkosi's suggestion I am getting these errors
<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
An error occurred when trying to create a controller of type 'CustomersController'. Make sure that the controller has a parameterless public constructor.
</ExceptionMessage>
<ExceptionType>System.InvalidOperationException</ExceptionType>
<StackTrace>
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) at System.Web.Http.Controllers.HttpControllerDescriptor.CreateController(HttpRequestMessage request)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
</StackTrace>
<InnerException>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Type 'Vidly.Controllers.Api.CustomersController' does not have a default constructor
</ExceptionMessage>
<ExceptionType>System.ArgumentException</ExceptionType>
<StackTrace>
at System.Linq.Expressions.Expression.New(Type type)
at System.Web.Http.Internal.TypeActivator.Create[TBase](Type instanceType)
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator)
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
</StackTrace>
</InnerException>
</Error>
Then I create a default constructor (without parameters as I understand) and then I get another error:
<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Object reference not set to an instance of an object.
</ExceptionMessage>
<ExceptionType>System.NullReferenceException</ExceptionType>
<StackTrace>
at Vidly.Controllers.Api.CustomersController.GetCustomers(String query) in C:\Users\Dovydas Petrutis\Desktop\vidly-mvc-5-master\Vidly\Controllers\Api\CustomersController.cs:line 26
at lambda_method(Closure , Object , Object[] )
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[]
methodParameters) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken) --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Dispatcher.HttpControllerDispatcher <SendAsync>d__1.MoveNext()
</StackTrace>
</Error>
Where could be the problem now?
First create a service that exposes the functionality you want so that it can be easily replaced/mocked for testing and improve maintainability.
You were close when you mentioned the repository. IHttpActionResult is a UI concern so you can refactor your interface like this.
public interface IAPICustomerRepository {
IEnumerable<CustomerDto> GetCustomers(string query = null);
CustomerDto GetCustomer(int id);
int CreateCustomer(CustomerDto customerDto);
CustomerDto UpdateCustomer(int id, CustomerDto customerDto);
bool? DeleteCustomer(int id);
}
The controller will now be slimmer in terms of functionality and no longer cares about EF/DbContext or Automapper.
public class CustomersController : ApiController {
private IAPICustomerRepository repository;
public CustomersController(IAPICustomerRepository repository) {
this.repository = repository;
}
// GET /api/customers
public IHttpActionResult GetCustomers(string query = null) {
var customerDtos = repository.GetCustomers(query);
return Ok(customerDtos);
}
// GET /api/customers/1
public IHttpActionResult GetCustomer(int id) {
var customer = repository.GetCustomer(id);
if (customer == null)
return NotFound();
return Ok(customer);
}
// POST /api/customers
[HttpPost]
public IHttpActionResult CreateCustomer([FromBody]CustomerDto customerDto) {
if (!ModelState.IsValid)
return BadRequest();
var id = repository.CreateCustomer(customerDto);
customerDto.Id = id;
return Created(new Uri(Request.RequestUri + "/" + id), customerDto);
}
// PUT /api/customers/1
[HttpPut]
public IHttpActionResult UpdateCustomer(int id, [FromBody]CustomerDto customerDto) {
if (!ModelState.IsValid)
return BadRequest();
var updated = repository.UpdateCustomer(id, customerDto);
if (updated == null)
return NotFound();
return Ok();
}
// DELETE /api/customers/1
[HttpDelete]
public IHttpActionResult DeleteCustomer(int id) {
var deleted = repository.DeleteCustomer(id);
if (deleted == null)
return NotFound();
return Ok();
}
}
With the controller now dependent on an abstraction you can mock the functionality when testing the controller in isolation.
Automapper is an implementation concern that can be encapsulated behind the abstraction so that it is a non issue when testing.
The following example uses Moq mocking framework. You can use your framework of choice is you so choose.
[TestClass]
public class CustomersController_Should {
[TestMethod]
public void GetCustomers() {
//Arrange
var fakeCustomers = new List<CustomerDto>{
new CustomerDto{ Id = 1 }
};
var repository = new Mock<IAPICustomerRepository>();
repository
.Setup(_ => _.GetCustomers(It.IsAny<string>()))
.Returns(fakeCustomers)
.Verifiable();
var controller = new CustomersController(repository.Object);
//Act
var result = controller.GetCustomers();
//Assert
repository.Verify();
//..other assertions
}
//...Other tests
}
The functionality that was originally in the controller would be encapsulated in the implementation of the repository in production.