August 2021 Note: Most of this post is no longer meaningful after the arrival of .NET Core with significant changes to the ASP.NET libraries. The note below about dropping WebForms and preferring MVC to write web apps is now completely outdated and redundant. The arrival of Blazor Webassembly has driven a stake into the heart of server-side ASP.NET apps. I can write a Blazor app in about one fifth the time and with one fifth the code compared to a similar ASP.NET app. Unfortunately, we are still stuck with ASP.NET Web API for RESTful data services (although Azure Function apps are a good alternative).
February 2017 Note : When I wrote this post a year ago I stated that I would not use ASP.NET MVC to write web applications, only WebAPI services. I have changed my mind and in future will drop WebForms and switch to MVC. I eventually got sick of the complicated pipeline of WebForms events and the lack of control over the rendered HTML, which is often an incomprehensible mess. I was also sick of getting hundreds of KB of ViewState for simple grids, and you can't disable the grid's ViewState or commands no longer fire.
However, if you create a skeleton MVC app from a Visual Studio 2015 template you get about 30 references to libraries and NuGet packages, most of which are JavaScript framework garbage. For a simple web app that needs little to no JavaScript you have to tediously remove packages and their dependencies in the correct order, then delete useless references and files. You can eventually strip the project down to a sensible size and start developing without all the clutter.
A few years ago I became fed-up with how complicated ASP.NET WebForms programming was. The pipeline of events is really long and delicate. If you do something in the wrong order you may get events that don't fire, controls that don't update, missing postback data, and so on. I've spent many tearful hours of my life trying to find out why a row click in a grid doesn't work to eventually discover I was doing something in the event handler that should have waited until PreRender, or vice versa. Another problem is the lack of control over formatting, exemplified by a tiny single-page app I wrote few months ago containing just a grid and a few buttons which renders an incomprehensible mess of html.
A few years ago, ASP.NET MVC was getting a lot of good comments and publicity so I ran the tutorials and bought some books in the hope that I would find the whole web programming model had been simplified with a tiny pipeline and you had more control over the rendered output. I found this to be generally true, but there was still "secret plumbing" all over the place. Magic values and naming conventions had simply replaced the difficulty of WebForms programming with a new type of mysterious difficulty. Everything I wanted to do in ASP.NET MVC required searching for a magical piece of plumbing that had to be named or registered.
After a few months I gave up in disgust at the prospect of using ASP.NET MVC. I just can't imagine what twisted minds can take something as simple as the http request-response model and wrap it in so many layers of complexity. I have been tempted at times to return to the early 1990s and write a web app with a single ashx handler file that takes the raw request and converts it into a raw response, thereby skipping all plumbing and frameworks.
Although I refuse to write web applications using ASP.NET MVC, I do write web services of the ASP.NET Web API variety which share a lot of coding style with MVC using routes and controllers. Even through the Web API is a stripped down MVC, it still unfortunately shares a fair amount of "secret plumbing" that you have to discover and use correctly. I have stumbled upon some important "secrets" which I'm putting in this post as a reminder to myself and anyone else who cares.
ActionFilterAttribute
Write a class that derives from ActionFilterAttribute and override OnActionExecuting and OnActionExecuted so you can automatically intercept the request and response of every API call. You can, for example, validate requests with code in a single place instead of spreading it all over the controller methods. You can extract request information like an auth token and place it in the request's property collection so it's handy at all times. Anything you want to intercept or do automatically on all API calls can be placed in your action filter class. Register the filter on controller methods like this example:[HttpDelete]
[Route("")]
[MyActionFilter(...)]
public IHttpActionResult Delete()
{
...
}
You can pass parameters to the filter's constructor if it's useful.
ExceptionFilterAttribute
Write a class that derives from ExceptionFilterAttribute and override OnException to automatically intercept all errors without the need to spread try-catch code all over the controllers. You can log errors or transform them into different types of responses that are perhaps more friendly for callers of your API. You register the exception filter the same way as the action filter.HttpParameterBinding
I uncovered this devious and tricky class while trying to automatically convert different types of query values into a controller method's bool parameter. It's quite sensible to allow API callers to specify a true/false value as Y, y, N, n, false, TRUE, 0, 1, and so on. I guessed there was a way of automatically coercing query values like &blah=1 or &blah=y into a bool value, but I had to find the secret plumbing that did this. An obscure hint in a web search pointed me to the HttpParameterBinding class, but further searches revealed ambiguous and confusing use of the class and the context parameters available to it. By trapping a call in the debugger I eventually managed to figure out just which context values were required to perform the transform I required. Here is the stripped down code that does what I want.public override Task ExecuteBindingAsync(...) { var dict = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.Query); string raw = dict[Descriptor.ParameterName]; bool? value = null; if (Regex.IsMatch(raw ?? "", $"^(?:y|true|1)$", RegexOptions.IgnoreCase)) { value = true; } else if (Regex.IsMatch(raw ?? "", $"^(?:n|false|0)$", RegexOptions.IgnoreCase)) { value = false; } actionContext.ActionArguments.Add(Descriptor.ParameterName, value ?? Descriptor.DefaultValue); return Task.FromResult(0); }
The raw string value named in Descriptor.ParameterName is parsed out of the request query string, inspected and transformed into the required bool value and placed in the actionContext.actionArguments collection for passing into the controller method. You can use this sort of parameter binding code to transform any incoming value into another type. You will need to create a small custom Attribute to apply your binding to a parameter, then use it like this:
internal class BoolBindingAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new BoolParameterBinding(parameter);
}
}
public IHttpActionResult Foo([MyBoolBinding] bool blah)
{
...
}
So if a request contains &blah=1 it will be silently transformed into true.
AddQueryStringMapping
It is a web service convention that the response body format obey the Accept request header MIME type value. So for example sending the header Accept: application/xml will result in an XML response body. For the convenience of callers, and for easier testing, it's possible to map a query parameter to a MIME type. In the static WebApiConfig Register method, add lines like these popular examples:config.Formatters.JsonFormatter.AddQueryStringMapping("format", "json", "application/json"); config.Formatters.XmlFormatter.AddQueryStringMapping("format", "xml", "application/xml");
You can then simply add ?format=xml to a request query to get the same effect as adding the corresponding request header.
No comments:
Post a Comment