Objective
When I started to create a mixed-use MVC and Web API project, I came across a lot of problems and shared it with everyone today. Have a friend private message I asked the project hierarchy and folder structure in my first blog is not clear, then I will be prepared from these files how to separate folder. The question is about:
1, the project Layer folder structure
2. Problems with controller class names that address MVC's controller and Web API
3. Registering different routes for areas of the MVC different namespaces
4. Let the Web API routing configuration also support namespace parameters
5. MVC and Web API add filters for authentication and error handling
6. MVC Add custom parameter model binding Modelbinder
7. Add custom parameter bindings to the Web API httpparameterbinding
8. Let the Web API support multiple get method bodies at the same time
I. Folder structure of the project layer
The structure of the talk about my own projects for your reference only, unreasonable places to welcome everyone to point out. In the first blog I have told you the bottom of the framework of the hierarchy and simply said the next project layer, and now we say carefully. When you create a new MVC or Web API, Microsoft has created many more folders for us, such as App_start Global settings, content styles, controller drop controllers, model data models, scripts scripts, and views. Some people are accustomed to the traditional three-layer architecture (some are n-tier), like the Model folder, controller folder, such as a separate project out, I feel is not necessary, because in different folders is also a kind of layered, alone out the most is compiled DLL is independent, Basically there is not much difference. So I am still a simple, the use of Microsoft's Good folder. Look at me first.
I added the area areas, my idea is the outermost model (deleted), Controllers, views are only put some common things, the real project in areas, such as MMS for my Material Management system, PSI is another system, SYS is my system Management module. So you can do multiple systems in a project, the reuse of the framework is self-evident. And look at one item in the area.
In this, Microsoft generated folders only controllers, Models, views. Others are I built, such as common put project common some classes, reports prepare to put the report file, ViewModels put Knouckoutjs viewmodel script file.
Let's take a look at where some of the controls introduced by the UI Library script library are placed. Such as
I put the framework CSS images JS themes etc are placed under the content, CSS placed in the project style and 960GS frame, JS below the core is self-defined some common JS including Utils.js, Common.js and Easyui knouckout binding implementation Knouckout.bindings.js, the other one to understand the basic do not introduce.
Second, solve the MVC controller and the Web API controller class name can not be the same problem
Back to a project folder under the Zone, in the Controller we will create the MVC controller and API controller, if a receiving business (receive)
The MVC route is registered as ~/{controller}/{action} and I want the access address to be ~/receive/action
The API is registered as ~/api/{controller} and I want the access address to be ~/api/receive
So the problem arises, Microsoft design This framework is through the class name to match the MVC you create a receivecontroller inherit controller, you can no longer create a Receivecontroller inheritance Apicontroller with the same name, In this way, the access address of MVC and API access address must have a name can not be called receive, is not very depressed.
By looking at the source code of Microsoft System.Web.Http, we found that this problem is also very good solution, in this Defaulthttpcontrollerselector class, Microsoft has defined the controller suffix,
We can solve the problem by changing the suffix of apicontroller to be different from MVC. This field is a static read-only field, so we just change it to Apicontrller to solve the problem. Our first thought must be reflection. Well, just do it, add the following code before registering the API route to complete
var suffix = typeof (Defaulthttpcontrollerselector). GetField ("Controllersuffix", BindingFlags.Static | BindingFlags.Public); if (suffix! = null) suffix. SetValue (NULL, "Apicontroller");
Iii. registering different routes for areas of the MVC different namespaces
This is good, MVC. The routing configuration supports namespaces, and when new zones are created, the framework automatically adds the {zone name}arearegistration.cs file, which is used to register the route for the region
Add the following code to the Registerarea method in this file to
Context. MapRoute (This . AreaName + "Default", this . AreaName + "/{controller}/{action}/{id}", new {controller = "Home", action = "Index", id = urlparameter.optional},
new string[] {"Zephyr.areas." + this. AreaName + ". Controllers "});
The fourth parameter is the namespace parameter, which indicates that the routing setting is valid only under this namespace.
Iv. let the Web API routing configuration also support namespace parameters
The headache is that the Web API routing configuration does not support namespace parameters, which indirectly makes me feel that it does not support area, Microsoft is really joking. All right, let's do it ourselves. Find an article on Google http://netmvc.blogspot.com/2012/06/aspnet-mvc-4-webapi-support-areas-in.html seems to be a wall, Here is a way to replace the Httpcontrollerselector service.
I directly paste my code out, you can directly use, first create a new Httpcontrollerselector class
using system;using system.linq;using system.collections.concurrent;using system.collections.generic;using System.net.http;using system.web.http;using system.web.http.controllers;using System.Web.Http.Dispatcher;using System.net;namespace zephyr.web{public class Namespacehttpcontrollerselector:defaulthttpcontrollerselector { Private Const string namespaceroutevariablename = "NamespaceName"; Private ReadOnly httpconfiguration _configuration; Private ReadOnly lazy<concurrentdictionary<string, type>> _apicontrollercache; Public Namespacehttpcontrollerselector (httpconfiguration configuration): Base (configuration) { _configuration = Configuration; _apicontrollercache = new lazy<concurrentdictionary<string, type>> (
New func<concurrentdictionary<string, type>> (Initializeapicontrollercache)); } private Concurrentdictionary<string, type> Initializeapicontrollercache () {Iassembliesre Solver assembliesresolver = this._configuration. Services.getassembliesresolver (); var types = this._configuration. Services.gethttpcontrollertyperesolver ()
. Getcontrollertypes (Assembliesresolver). ToDictionary (t = t.fullname, t = t); return new concurrentdictionary<string, type> (types); } public ienumerable<string> Getcontrollerfullname (httprequestmessage request, String controllername) {Object namespacename; var data = Request. Getroutedata (); ienumerable<string> keys = _apicontrollercache.value.todictionary<keyvaluepair<string, Type>, string , type> (t = t.key, t = t.value, stringcomparer.currentcultureignorecase). Keys.tolist (); if (!data. Values.trygetvalue (Namespaceroutevariablename, out namespacename)) {return from K in keys where K.endswith (string. Format (". { 0}{1} ", Controllername,
defaulthttpcontrollerselector.controllersuffix), stringcomparison.currentcultureignorecase) Select K; } string[] namespaces = (string[]) namespacename; Return from N in namespaces joins K in keys on string. Format ("{0}.{ 1}{2} ", N, Controllername,
defaulthttpcontrollerselector.controllersuffix). ToLower () equals K.tolower () select K; } public override Httpcontrollerdescriptor Selectcontroller (Httprequestmessage request) {Type Ty Pe if (request = = null) {throw new ArgumentNullException ("request"); } string controllername = this. Getcontrollername (Request); if (string. IsNullOrEmpty (Controllername)) {throw new httpresponseexception (request. Createerrorresponse (Httpstatuscode.notfound, String. Format ("No route providing a controller name is found to match request URI ' {0} '",
New object[] {request. RequestUri})); } ienumerable<string> fullnames = getcontrollerfullname (request, controllername); if (fullnames.count () = = 0) { throw new httpresponseexception (request. Createerrorresponse (Httpstatuscode.notfound,
New object[] {request. RequestUri})); } if (This._apicontrollercache.value.trygetvalue (Fullnames.first (), out type)) { return new Httpcontrollerdescriptor (_configuration, controllername, type); } throw new Httpresponseexception (request. Createerrorresponse (Httpstatuscode.notfound,
New object[] {request. RequestUri})));}}}
Then replace the service in the register of the Webapiconfig class to achieve
Config. Services.replace (typeof (Ihttpcontrollerselector), new Namespacehttpcontrollerselector (config));
OK, now look at how to register the route of the API in the Registerarea method under the {Areaname}arearegistration Class of the zone:
GlobalConfiguration.Configuration.Routes.MapHttpRoute (This . AreaName + "Api", "api/" + this. AreaName + "/{controller}/{action}/{id}",
NamespaceName = new string[] {string. Format ("zephyr.areas.{ 0}. Controllers ", this. AreaName)}}, new {action = new Startwithconstraint ()});
The third parameter defaults in the NamespaceName, the above service has implemented support. The fourth parameter constraints I will mention in the 8th question, this is the first one to skip.
V. MVC and Web APIs add filters for authentication and error handling
First, the issue of authentication. Either the MVC or the API has a security issue that does not pass the authentication to the person who can access the issue. When we are new to an empty project, the default is no authentication, unless you add the authorize property on the Controller class or method to require authentication. But my controller has so many, I have to add properties to it, much trouble, so we think of the filter. After adding the filter, the controller does not have to add the equivalent of this property.
MVC adds the following code directly to the Registerglobalfilters method of the Filterconfig class
Filters. ADD (New System.Web.Mvc.AuthorizeAttribute ());
The Web API filter does not have a single configuration class, which can be written in the register of the Webapiconfig class
Config. Filters.add (New System.Web.Http.AuthorizeAttribute ());
MVC error handling defaults to adding Handleerrorattribute default filters, but we may want to catch this error and log the system log then this filter is not enough, so we want to customize the MVC and the Web API's respective error handling classes, the following stickers my error handling , Mvchandleerrorattribute
Using system.web;using system.web.mvc;using log4net;namespace zephyr.web{public class Mvchandleerrorattribute: Handleerrorattribute {public override void Onexception (Exceptioncontext filtercontext) { ILog log = Logmanager.getlogger (FilterContext.RequestContext.HttpContext.Request.Url.LocalPath); Log. Error (filtercontext.exception); Base. Onexception (Filtercontext);}}}
Error handling for WEB APIs
Using system.net;using system.net.http;using system.web;using system.web.http.filters;using log4net;namespace zephyr.web{public class Webapiexceptionfilter:exceptionfilterattribute {public override void Onexception (Httpactionexecutedcontext context) { ILog log = Logmanager.getlogger ( HttpContext.Current.Request.Url.LocalPath); Log. Error (context. Exception); var message = context. Exception.Message; if (context. Exception.innerexception = null) message = context. Exception.InnerException.Message; Context. Response = new Httpresponsemessage () {Content = new stringcontent (message)}; Base. Onexception (context);}} }
Then register them separately in the filter, in the Registerglobalfilters method of the Filterconfig class
Filters. ADD (New Mvchandleerrorattribute ());
In the register of the Webapiconfig class
Config. Filters.add (New Webapiexceptionfilter ());
So the filter is defined.
VI. MVC Add custom model bindings Modelbinder
In MVC, we may be able to customize some of the parameters we want to receive, so we can do it through Modelbinder. For example, I want to receive the Jobject parameter in the MVC method
Public Jsonresult DoAction (Dynamic request) {}
If you write this directly, the received request is NULL, because the Jobject type parameter MVC is not implemented, we have to implement it ourselves, first create a new Jobjectmodelbinder class, add the following code implementation
Using system.io;using system.web.mvc;using newtonsoft.json;namespace zephyr.web{public class Jobjectmodelbinder : Imodelbinder {Public object Bindmodel (ControllerContext controllercontext, Modelbindingcontext BindingContext) { var stream = ControllerContext.RequestContext.HttpContext.Request.InputStream; Stream. Seek (0, seekorigin.begin); String json = new StreamReader (stream). ReadToEnd (); Return jsonconvert.deserializeobject<dynamic> (JSON);}}}
Then add the following MVC registration route
ModelBinders.Binders.Add (typeof (Jobject), New Jobjectmodelbinder ()); For dynamic Model binder
Once added, we can receive the Jobject parameter in the MVC controller.
Vii. WEB API Add custom parameter bindings httpparameterbinding
Do not know what the Devil, the Web API parameter binding mechanism is very different from the MVC parameter binding, the first Web API binding mechanism is divided into two kinds, called model binding, a kind of called formatters, the general model The binding is used to read the value in the query string, and formatters is used to read the value in the body, and there's a lot of things to dig into, and we're interested in going back to it, so here's a simple way to customize modelbinding, like in the Web In the API I have defined a class called Requestwrapper, I want to receive Requestwrapper parameters in the API controller, as follows
Public dynamic Get (requestwrapper query) { //do something}
So we're going to create a new requestwrapperparameterbinding class
using system.collections.specialized;using system.threading;using system.threading.tasks;using System.web.http.controllers;using system.web.http.metadata;using zephyr.core;namespace Zephyr.Web{public class requestwrapperparameterbinding:httpparameterbinding {private struct Asyncvoid {} public Requestwrappe Rparameterbinding (httpparameterdescriptor desc): Base (DESC) {} public override Task Executebindingasync (Modelmeta Dataprovider Metadataprovider,
Httpactioncontext Actioncontext, CancellationToken cancellationtoken) { var request = System.Web.HttpUtility.ParseQueryString (actionContext.Request.RequestUri.Query); var requestwrapper = new Requestwrapper (new NameValueCollection (Request)); if (!string. IsNullOrEmpty (request["_xml")) { var xmlType = request["_xml"]. Split ('. '); var Xmlpath = string. Format ("~/views/shared/xml/{0}.xml", xmltype[xmltype.length–1]); if (Xmltype.length > 1) xmlpath = string. Format ("~/areas/{0}/views/shared/xml/{1}.xml", xmlType); Requestwrapper.loadsettingxml (Xmlpath); } SetValue (Actioncontext, requestwrapper); taskcompletionsource<asyncvoid> TCS = new taskcompletionsource<asyncvoid> (); Tcs. Setresult (Default (Asyncvoid)); Return TCS. Task;}}}
The next step is to register this binding with the binding rule or add it to the Webapiconfig
Config. Parameterbindingrules.insert (0, param = { if (param. ParameterType = = typeof (Requestwrapper)) return new requestwrapperparameterbinding (param); return null;});
Now that the Requestwrapper parameter binding is complete, you can use the
Eight, let the Web API support multiple get methods simultaneously
First quote Microsoft official things to the existence of the problem with everyone to understand, if the Web API in the route registered as
Routes. Maphttproute ( name: "API Default", routetemplate: "Api/{controller}/{id}", defaults:new {id = Routeparameter.optional});
Then my controller is
public class productscontroller:apicontroller{public void Getallproducts () {} public ienumerable<product > getproductbyid (int id) {} public httpresponsemessage deleteproduct (int id) {}}
Then the corresponding address request method is as follows
See the above do not know that the people see the problem no, if I have two Get method (I add a gettop10products, this is very common), and the parameters are the same then there is no way to differentiate the route. Someone just thought about it. Modify the routing settings, put routetemplate: modified to "Api/{controller}/{action}/{id}", yes, this is able to solve the above problem, but your api/products either get Delete Post Input mode can not request to the corresponding method, you have to Api/products/getallproducts, API/PRODUCTS/DELETEPRODUCT/4, action name you can not omit. Now I understand the problem. I just want to solve this problem.
Remember when I wrote the 4th, I mentioned here, the idea is to define a constraints to achieve:
We first analyze the URI path:api/controller/x, the problem is here in the X, it may represent the action also may represent the ID, in fact, we are to distinguish this x under what circumstances represents the action in which case the ID can solve the problem, I want to define a system verb myself, if your actoin name is one of those verbs that I define, then I think you are action, otherwise think you are an ID.
OK, the idea is clear, we begin to implement, first define a Startwithconstraint class
using system;using system.collections.generic;using system.linq;using system.web;using System.Web.Http.Routing ; namespace zephyr.web{//<summary>///If the request URL such as: api/area/controller/x x may be actioin or ID///The x position in the URL appears A string that begins with a get put delete post, or as an action, or as an ID///If the action is empty, assign the request method to action//</summary> public class Sta Rtwithconstraint:ihttprouteconstraint {public string[] array {get; set;} public bool Match {get; set;} private string _id = "id"; Public Startwithconstraint (string[] Startwitharray = null) {if (Startwitharray = = null) Startwitharray = new string[] {"GET", "PUT", "DELETE", "POST", "EDIT", "UPDATE", "AUDIT", "DOWNLOAD"}; This.array = Startwitharray; public bool Match (System.Net.Http.HttpRequestMessage request, Ihttproute route, String parametername,
Idictionary<string, object> values, httproutedirection routedirection) {if (values = = NULL)//shouldn ' t ever hits this. return true; if (!values. ContainsKey (parametername) | | !values. ContainsKey (_id))//Make sure the parameter is there. return true; var action = Values[parametername]. ToString (). ToLower (); if (string. IsNullOrEmpty (Action))//If the Param key is an empty in this case "action" add the method so it doesn ' t hits other methods L Ike "GetStatus" {values[parametername] = Request. Method.tostring (); } else if (string. IsNullOrEmpty (values[_id]. ToString ())) {var isidstr = true; Array. ToList (). ForEach (x = = {if (action. StartsWith (X.tolower ())) Isidstr = false; }); if (ISIDSTR) {values[_id] = Values[parametername]; Values[parametername] = Request. Method.tostring (); }} return true; } }}
Then add a fourth parameter when the corresponding API route is registered constraints
GlobalConfiguration.Configuration.Routes.MapHttpRoute (This . AreaName + "Api", "api/" + this. AreaName + "/{controller}/{action}/{id}",
NamespaceName = new string[] {string. Format ("zephyr.areas.{ 0}. Controllers ", this. AreaName)}}, new {action = new Startwithconstraint ()});
This is achieved, the API controller is the name of the action is to pay attention to the point is, but it is a relatively perfect solution. Described later
The problem with the MVC framework is similar, the next time you may start to write some specific pages and details, if you are interested in my framework, I will continue to write down. However, from the day before yesterday, I have written three days of the blog, continuously endure a few nights so late, I decided I have to take a few days off.
MVC Routing Configuration