MVC application: json + view output - asp.net-mvc

I developed an mvc-razor app the classical way: I do some computation in my controller and show the results in the view.
Now, in addition to the formatted output I already provide through the view, I need to add the ability to provide the same results in json, so that my controller acts as a service.
Something like
if (json == true)
{
return JsonOutput(model);
}
else
{
return View(model);
}
Since I don't want to reinvent the wheel, I wonder if there is a standard approach to this task.
Thank you

What you are looking for is an ApiController:
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase { ... }
In this controller you will define your action methods, very much like in Mvc, but not return a View:
[HttpGet]
public async Task<IActionResult> Get()
{
/* ... retrieve results*/
return Ok(yourPayload);
}
I suggest you follow along the excellent documentation: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-5.0&tabs=visual-studio

Related

Implementing data interface in ASP.NET MVC Controller and returning JSON

I would like to have my Controller implement my data interface that returns custom objects. All the objects are serializable and decorated by JSON attributes. So I would like to have my controller method simply be that:
public class MyController : Controller, IMyInterface
{
public Foo[] GetFoosByName(string name)
{
return new Foo[]{new Foo(name), new Foo(name)}
}
}
If I do it I get simply "Foo[]" response. What I'd like to get is JSON-serialized Foo objects.
I can easily achieve this by changing my response to be JsonResult:
public JsonResult GetFoosByName(string name)
{
return Json(new Foo[]{new Foo(name), new Foo(name)});
}
But then I won't be able to implement IMyInterface in the way that is easily maintainable.
Any ideas on how I can automatically get the behavior as I was returning JsonResult, but still keeping original return types?
Generally I would recommend against such a pattern. An MVC Controller should ideally be kind of a top-most layer, and I think it should not implement interfaces. You should implement such interfaces in a service layer, below the MVC Controllers.
However, if you still want to do it so, you can use explicit implementation like this.
public class MyController : Controller, IMyInterface
{
public JsonResult GetFoosByName(string name)
{
return Json(((IMyInterface)this).GetFoosByName(name));
}
Foo[] IMyInterface.GetFoosByName(string name)
{
return new[] { new Foo(name) };
}
}

How should I design MVC to conditionally return JSON or pretty HTML?

I have data that will either be consumed by a human or a web service. The REST url is:
http://host.com/movies/detail/1
http://host.com/movies/detail/The Shawshank Redemption (1994)
What convention should I follow to conditionally return JSON or HTML? Should I add a parameter such as "?json" or should I look at the client headers,.. some variation of both?
If I do a variation of both, if a conflict is found which takes precedent?
Check whether the Request is Ajax. You may use the Request.IsAjaxRequest() method which returns true/false.
public ActionResult details(string id)
{
var movieViewModel=movieService.GetMovieDetails(id);
If(Request.IsAjaxRequest())
{
// return Json now
return Json(movieViewModel,JsonRequestBehavior.AllowGet);
}
// Not an ajax request, Let's return Normal View (HTML)
return View(movieViewModel);
}
UNIT TESTING ASPECT : Request.IsAjaxRequest() is not unit test friendly! So if you are worried about unit tests, You can write your IsAjaxRequest property and put in your basecontroller class and use it.
public class BaseController : Controller
{
protected bool IsAjaxRequest()
{
//Using this method instead of Request.IsAjaxRequest() because
//IsAjaxRequest is static and not mockable ! (not unit test friendly)
var isAjax = Request.Headers["X-Requested-With"];
if (isAjax != null && isAjax.ToUpper() == "XMLHTTPREQUEST")
return true;
return false;
}
}
Now inherit your controller from this BaseController.
public class HomeController : BaseController
{
public ActionResult details(string id)
{
var movieViewModel=movieService.GetMovieDetails(id);
If(IsAjaxRequest)
{
// return Json now
return Json(movieViewModel,JsonRequestBehavior.AllowGet);
}
// Not an ajax request, Let's return Normal View (HTML)
return View(movieViewModel);
}
}
You could also use:
[HttpGet]
public ActionResult() {
// Return HTML
}
[HttpPost]
public ActionResult() {
// Assuming Ajax is of the type post
}
Just another solution if all your Ajax is using post.
I prefer using a parameter explicit in the URL because that way building REST petitions is easy for developers, self explanatory and with no surprises (do you have to guess default format? or see "difficult" to see HTTP headers). You decide:
if you have many options for formats you can use format=json
you can go with json parameter, but it is not pretty because you have to pair it with a value json=true, json=1. Besides you can set json=1&xml=1&html=1, harder to handle.
the twitter way is to emulate an extension such as call.json or call.xml (e.g. https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline)
I recommend don't tie together a kind of petition and a format. Let your API clients decide, ajax-json is commonly used, but not all develepers use it that way. Maybe I am writing a terminal application with no ajax, maybe I want to do a wget to your API and still get json.

ASP.NET MVC Posting models to an action with an an Interface

My understanding is that ASP.NET MVC only allows you to POST objects to Actions in the Controller, where the Action's arguments accept the posted object as a Concrete class.
Is there any way around this, or a good alternative?
In my case, I have an action which accepts an interface as an argument:
public ActionResult SaveAdjustment(IModel model)
{
switch (model.SubsetType)
{
// factory like usage
}
}
And for this action, I have numerous views, all strongly typed to objects that implement IModel, all which I want to be able to post to this one method.
Of course, running this give me the error:
Cannot create an instance of an interface
Is there a nice work around to this? Or do I need to create an Action method for each and send them over to a method like this?
MVC generally binds models when posting from Request.Form, that is collection of name=value pairs. The reason that in default implementation there's no support of binding interfaces or abstract classes is obvious - mvc cannot determine which concrete class to create from name=value pairs. If you got hidden field on client side, or any other parameter anywhere by which you are able to determine which type of concrete class to create, you can simply create custom model binder. I believe you can override DefaultModelBinder's CreateModel method and reuse all other built in binding functionality
public class IModelModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
//Create and return concrete instance
}
}
And model binder registration in global.asax
ModelBinders.Binders.Add(typeof(IModel?), new IModelModelBinder());
Actually, controllers and actions in mvc are meant to be thin, and some kind of service layer should be thick. As action logic you are trying to implement may get complicated soon, I would recommend moving it into separate service.
Although I mentioned this as a possible solution in my original question, its the solution I have gone with in the end and I actually quite like it now. This way I didn't need to touch the model default binding implementation and I think this approach is a more readable/understandable approach than what I was originally asking for.
In case its not clear why I wanted to go for this approach, I have added an example of how I can use this for its OO benifits.
[HttpPost]
public ActionResult SaveModelA(ModelA model)
{
return SaveModel(model);
}
[HttpPost]
public ActionResult SaveModelB(ModelB model)
{
return SaveModel(model);
}
private ActionResult SaveModel(IModel model)
{
IExampleService exampleService;
IRequirements requirements;
switch (model.SubsetType)
{
case SubsetType.ModelA:
myService = new ModelAService();
requirements = new ModelARequirements
{
ModelASpecificProperty = "example"
};
break;
case SubsetType.ModelB:
myService = new ModelBService();
requirements = new ModelBRequirements
{
ModelBSpecificProperty1 = "example",
ModelBSpecificProperty2 = "example2",
ModelBSpecificProperty3 = "example3"
};
break;
default:
throw new InvalidEnumArgumentException();
}
var serviceResonse = exampleService.ExecuteExample(model, requirements);
return RedirectToAction("Index", new
{
ExampleData = serviceResponse.ExampleDate
});
}
In case it isn't clear in the code:
ModelA : IModel
ModelB : IModel
ModelARequirements : IModelRequirements
ModelBRequirements : IModelRequirements
ModelAService : IExampleService
ModelBService : IExampleService
// and IModel defines a property SubsetType SubsetType { get; }

How to make ASP.NET MVC Action return different formats?

I don't know if this is the right way to approach something, but I'm hoping it is. The example below is a heavy controller and is absolutely the wrong approach, but it get's the idea of what I'm looking for across.
public class PeopleController : Controller
{
public ActionResult List(string? api)
{
MyViewModel Model = new MyViewModel();
if (api == "json") {
// I'd like to return the Model as JSON
} else if (api == "XML") {
// I'd like to return the Model as XML
} else {
return View(Model);
}
}
}
Now what I need to be able to do is return the Model to the View if it's being requested like this:
http://example.com/People/List
But I'd like it to output JSON if it's requested like this:
http://example.com/People/List/?api=json
Or output XML if it's requested like this:
http://example.com/People/List/?api=xml
Is this just plain wrong? If not, what is the best approach to achieve this?
I was thinking of achieving it with a Custom MultiPurposeResult that could do all the filtering for me and then return it as this
public class PeopleController : Controller
{
public MultiPurposeResult List(string? api)
{
MyViewModel Model = new MyViewModel();
return MultiPurpose(Model); }
}
}
Agree with #Matt. Don't explicitly ask for the response type, infer it from the contentType in the request, which is more RESTful.
For example, created a basic enum type to encapsulate the response types you want:
public enum RestfulResultType
{
Json,
Html,
Xml
}
Then create a custom model binder than sets this property in your action, depending on the content type.
Then your controller could look like this:
public ActionResult List(RestfulResultType resultType)
{
var data = repo.GetSomeData();
switch (resultType)
{
case RestfulResultType.Json:
return Json(data);
case RestfulResultType.Xml:
return XmlResult(data); // MvcContrib
case RestfulResultType.Html:
return View(data);
}
}
If you need any more customization than the regular helpers provide, then create custom ActionResult's.
You can leave the return type as ActionResult - that's the point, so that the controller can return different formats.
ResfulResultTypeModelBinder.cs:
public class ResfulResultTypeModelBinder: IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.HttpContext.Request.ContentType == "application/json")
return RestfulResultType.Json;
// other formats, etc.
}
}
Global.asax:
ModelBinders.Binders.Add(typeof(RestfulResultType), new RestfulResultTypeModelBinder());
You can create a custom MultiPurposeResult but I personally would lose the string? api from the method signature, instead have the MultiPurpose look for the presence of Request["format"] and then make the determination of what format to possible output the results in. Since the format doesn't nessecarily have anything to do with the ActionResult but more the format of the response.

How to use HandleError with model state errors

I want to use a custom action filter to handle specific exceptions from my service classes to populate the model state and then return the view.
For example, take my previous code:
public ActionResult SomeAction(SomeViewModel model)
{
try
{
_someService.SomeMethod(model);
}
catch (ServiceException ex)
{
ModelState.AddModelError(ex.Key, ex.ErrorMessage);
}
return View();
}
Basically, it would call a service, and if a ServiceException was thrown, it would know that there was an issue w/ the model data, and add the error to the ModelState, then just return the view. But I noticed some very un-DRY-like patterns, because I had this same try/catch code in every action method.
So, to DRY it up a bit, I basically created a new HandleServiceError action filter:
public class HandleServiceErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext context)
{
((Controller)context.Controller)
.ModelState
.AddModelError(
((ServiceException)context.Exception).Key,
((ServiceException)context.Exception).ErrorMessage
);
context.ExceptionHandled = true;
}
}
Then simplified my action methods like so:
public ActionResult SomeAction(SomeViewModel model)
{
_someService.SomeMethod(model);
return View();
}
Problem is, once the action filter handles the error, it doesn't return to my action method. I sort of understand, under the hood, why this is happening. But I would still like to figure out a way to do what I'm trying to do.
Is this possible?
Thanks in advance.
UPDATE:
I tried the suggestions from the article Darin provided in his answer, but ran into issues trying to use constructor injection with the controller's model state.
For example, if you look at their Controllers\ProductController.cs code, they have the controller's empty constructor using a service locator to create the service, passing in the controller's ModelState at that point:
public ProductController()
{
_service = new ProductService(new ModelStateWrapper(this.ModelState),
new ProductRepository());
}
But if you look at the injected constructor, it assumes the ModelState will be injected into the constructor for the service:
public ProductController(IProductService service)
{
_service = service;
}
I don't know how to get CI to work with the current controller's ModelState. If I could figure this out, then this approach may work.
You could still return the corresponding view:
context.Result = new ViewResult
{
ViewName = context.RouteData.GetRequiredString("action")
};
You may also take a look at the following article for an alternative about how to perform validation at the service layer.

Resources