ASP.NET Core MVC/WebApi Basic Series 2

Time:2019-9-3

> Preface

I haven’t bubbled for a long time. It’s estimated that I haven’t updated my blog for nearly half a year. It’s the first time that I stopped blogging for so long. People are always lazy. The longer the time, the lazier the lazier. But it’s impossible not to learn, and the continuous inertia is not dei. Otherwise, it will be abandoned by time and eliminated by technology. Okay, enter today. Topics, this section, let’s talk about. NET Core’s model binding system, model binding principles, custom model binding, hybrid binding, ApiController nature, some of you have seen, but the effect is not very good, this is the most detailed explanation, I suggest that I have learned to publish the course. Look at the children’s shoes too. This article is a little long. Please be patient. I will only talk about what you can use or learn.

Model Binding System

For model binding, [BindRequired], [BindNever], [FromHeader], [FromQuery], [FromRoute], [FromForm], [FromServices], [FromBody] and other features, [BindRequired] and [BindNever] are translated as having to bind, never binding what we call behavioral binding, but following five From, Where to translate it into source binding, we call it source binding. Next, we describe these two binding types in detail. This section uses version. NET Core 2.2.

Behavior binding

[BindRequired] means that the key of the parameter must be provided, but it does not care whether the value of the parameter is empty, [BindNever] means ignoring the binding of attributes. Behavior binding seems simple, but it is not. When I talk about it, let’s first look at the following code snippet.


public class Customer
 {
 [BindNever]
 public int Id { get; set; }
 }

 [Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post(Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

Above all, we define a Customer class, then the ID field in the class is identified by the [BindNever] feature, and then we all make requests through Postman.

When we send the request as above, the response will return the status code 200 successfully and the ID is not bound, which meets our expectation. It means that the attribute ID is never bound. Now let’s add the [FromBody] identifier to the Post method parameter on the controller. The code snippet becomes as follows:


[HttpPost]
 public IActionResult Post([FromBody]Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }

This is why, after we identify the property with the [FromBody] feature, we add the property ID with the [BindNever] feature (code is the same as above, no duplicate posting). As a result, the ID is bound, indicating that the [BindNever] feature is invalid for parameters identified by the [FromBody] feature. Is that really the case? Next we try to bind [BindNever] to an object, as follows:


public class Customer
 {
 public int Id { get; set; }
 }

 [Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post([BindNever][FromBody]Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

Above all, we bind [BindNever] to the object Customer, and there is no order for [BindNever] and [FromBody] features. That is to say, we can put [FromBody] after [BindNever]. Next, we use Postman to send the following request again.

At this point, we can clearly see that the request we send contains the ID field, and when we bind [BindNever] to the object, the final ID is not bound to the object, which meets our expectations and passes the validation, but in other words, it makes no sense to bind [BindNever] to the object because all the attributes on the object are meaningless at this time. They will be ignored. So here we can conclude that [BindNever] requests for [FromBody] features:

For requests identified by the [FromBody] feature, [BindNever] feature is applied to the attributes on the model, and the binding is invalid. When applied to the model object, all attributes on the model object are completely ignored.

For requests from URLs or forms, [BindNever] features are applied to attributes on the model, where the binding is invalid, and when applied to model objects, all attributes on the model object are completely ignored.

Okay, let’s look at [BindRequired], and we’ll continue with the following code:


public class Customer
 {
 [BindRequired]
 public int Id { get; set; }
 }

 [Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post(Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

By identifying attributes with the [BindRequired] feature, we base our request on the form and do not give the value of the attribute ID. At this time, the attribute is not bound and the validation is not passed, which is in line with our expectation. Next, let’s look at the request for the [FromBody] feature identifier. The code is not given. We just add [FromBody] to the object. Let’s see the final result.

At this point, it seems to have met our expectations on the surface. Even if we do not specify the BindRequired feature for the attribute id, the result is the same. This is why, by default, the object identified by the [FromBody] feature in. NET Core can not be empty and built-in is processed. Make the following settings to allow null.


services.AddMvc(options=> 
  {
  options.AllowEmptyInputInBodyModelBinding = true;
  }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

After we do the above settings, we do not give the value of the attribute id, we will certainly verify that it is correct, then we will give an attribute Age, and then make a request that does not contain the Age attribute, as follows


public class Customer
 {
 [BindRequired]
 public int Id { get; set; }

 [BindRequired]
 public int Age { get; set; }
 }

Here we find that we have added the BindRequired feature to the attribute Age, but the validation passes. Let’s think about it again. Perhaps the attribute Age given by us has a default value of int of 0, so the validation passes. Good idea is that you can continue to add a string type attribute and then add the BindReq. Uired] feature, which is not included in the final request, and the result is still validated (try it yourself).

At this point, we find that the request identified by the [FromBody] feature excludes the case that the default object is not empty, indicating that the attribute of the [BindRequired] feature identifier is invalid for the request identified by the [FromBody] feature. At the same time, we turn to the definition of the [BindRequired] feature to explain as follows:

// Summary:
// Indicates that a property is required for model binding. When applied to a property, the model binding system requires a value for that property. When applied to
// a type, the model binding system requires values for all properties that type defines.

It’s easy to understand that when we use the BindRequired feature identifier, it shows that attributes must be given in model binding. When applied to attributes, the model binding system is required to verify that the value of this attribute must be given. When applied to types, the model binding system is required to verify what is defined in the type. Attributes must have value. This explanation makes us unconvinced that requests based on URLs or forms are distinctly different from requests based on FromBody features, but the definitions are uniform. At this point we missed a Required feature. We added an Address attribute, and then the request did not contain the Address attribute.


public class Customer
 {
 [BindRequired]
 public int Id { get; set; }
 [BindRequired]
 public int Age { get; set; }
 [Required]
 public string Address { get; set; }
 }

As can be seen from the figure above, requests identified with the [FromBody], attributes identified through the Required feature also meet expectations, of course, for URL and form requests, which are not demonstrated here. I haven’t looked at the source code. I dare to guess if the following reasons make a difference (personal guess).

Explanations emphasize model binding systems, so the BindNever and BindRequired features in. NET Core are designed specifically for. NET Core MVC model binding systems, and for the FromBody feature identification, because their serialization and deserialization are related to Input Former, such as through JSON. NET, So whether or not we ignore and map attributes depends on whether we use serialization and deserialization frameworks, which we define ourselves, such as using JSON. NET to ignore attributes [JsonIgnore].

So whether a request based on the FromBody feature identification maps or not depends on the serialization and deserialization framework we use. The default in. NET Core is JSON. NET, so we need to use the Api in JSON. NET for the above attributes, such as the following.


public class Customer
 {
 [JsonProperty(Required = Required.Always)]
 public int Id { get; set; }

 [JsonProperty(Required = Required.Always)]
 public int Age { get; set; }
 }

Request parameter security is also a factor we need to consider, such as the following: our object contains the IsAdmin attribute, our background will judge whether to render the UI for the corresponding role according to the value of the attribute. We can apply the [Bind] feature to the object to specify which attributes to map, at which time the parameters in the request even explicitly specify the attributes. The parameter values are not mapped (here is just an example, which may not be reasonable). The code is as follows:


public class Customer
 {
 public int Id { get; set; }
 public int Age { get; set; }
 public string Address { get; set; }
 public bool IsAdmin { get; set; }
 }

 [Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post(
  [Bind(nameof(Customer.Id),nameof(Customer.Age),nameof(Customer.Address)
  )] Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

Source binding

There are different features in. NET Core, such as the behavioral binding we described above, and then the source binding we’re going to talk about next. What’s the significance and role of these features? It’s more flexible than model binding in. NET, but not the same. Why is flexibility not what I said. It’s proved by practical examples that every new function or feature appears to solve the corresponding problems or improve the corresponding problems. First, let’s look at the following code:


[Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost("{id:int}")]
 public IActionResult Post(int id, Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

We specify ID 4 by routing, and then 3 by url. Do you guess the result of the parameters mapped to background ID is 4 or 3, and the parameter ID on customer is 4 or 3?

From the figure above, we can see that the ID is 4, and the ID value in the customer object is 2. What conclusion can we draw from it? Let’s summarize the following.

In. NET Core, by default, parameter binding has priority, routing priority is higher than form priority, and form priority is higher than URL priority (routing > Form > URL).

This is the priority by default. Why is it very flexible in. NET Core because we can explicitly bind it by source, such as forcing the ID to come from the query string, and the ID in customer comes from the query routing, as follows:


[HttpPost("{id:int}")]
 public IActionResult Post([FromQuery]int id, [FromRoute] Customer customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }

What other source bindings [FromForm], [FromServices], [FromHeader] are mandatory to specify whether the parameters originate from the form, request header, query string, routing or Body, so I don’t need to explain more here. An example is enough to illustrate its flexibility.

Model binding (examples of strong support)

We recognize the flexibility of source binding. Some children’s shoes probably don’t know at all. NET Core’s strong support for model binding, where is strong. Before explaining the principle of model binding, let’s give you some practical examples to illustrate. First, let’s look at the following request code:

For the above request, most of our practice is to accept the above URL parameters by creating a class as follows.


public class Example
 {
 public int A { get; set; }
 public int B { get; set; }
 public int C { get; set; }
 }

 [Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpGet]
 public IActionResult Post(Example employee)
 {
  return Ok();
 }
 }

This common practice is also supported in ASP.NET MVC/Web Api. Okay, next we will modify the above controller code to support it in. NET Core, but not in. NET MVC/Web Api. Believe it or not, you can try.


[Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpGet]
 public IActionResult Get(Dictionary<string, int> pairs)
 {
  return Ok();
 }
 }

As for why we can bind in. NE Core, we mainly implement Dictionary Model Binder in. NET Core, so we can use the parameters on the URL as the keys of the dictionary, and the parameter values as the corresponding values of the keys. It’s not interesting to see, right, good. Next, let’s look at the following requests. How do you think the controller should connect them? What about parameters on the URL?

Give your imagination a bold play, how do we receive the parameters on the above URL in our controller action method? Okay, no more sales.


[Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpGet]
 public IActionResult Post(List<Dictionary<string, int>> pairs)
 {
  return Ok();
 }
 }

Does. NET Core not support it? Obviously not, we need to modify the parameter name to be consistent. We change the parameter name on the URL to be consistent with the parameter on the controller method (of course, the type is also consistent, otherwise it will not map). Here is the following:

Okay, see how powerful the model binding system is in. NET Core. Next, let’s take a quick look at how model binding works, GO.

Principle of Model Binding

What’s the use of understanding the principles of model binding? When the model binding system provided by. NET Core does not meet our requirements, we can customize the model binding to meet our requirements. Here I briefly describe the whole process, and then give a detailed picture of the whole process of model binding. When we use the services. AddMvc () method in startup, we will use the MVC framework. What is the model binding behind this time?

[1] Initialize the ModelBinderProviders collection and add 16 implemented ModelBinderProviders to it

[2] Initialize the ValuesProviderFactories collection and add four ValueFactory to it

[3] Inject < IModel BinderFactory, Model BinderFactory > in a single case

[4] Adding metadata information for other models

How do you bind parameters next? First, we define an IModelBinder interface, as follows:


 public interface IModelBinder
 {
 Task BindModelAsync(ModelBindingContext bindingContext);
 }

So what is the interface used for? We know from the method name defined in the interface that this is the final ModelBinder that we get, and then bind parameters by binding context. So what about the specific ModelBinder? Next, the IModelBinderProvder interface is defined as follows:


public interface IModelBinderProvider
 {
 IModelBinder GetBinder(ModelBinderProviderContext context);
 }

Get the specific Model Binder through the Model Binder Provider Context in the IModelBinderProvider interface. Then how do we get the specific Model Binder through the method GetBinder in the interface? In other words, how do we create the specific Model Binder? When adding the MVC framework, we inject the Model BinderFactory. At this point, Model BinderFactory comes on, code as follows:


public class ModelBinderFactory : IModelBinderFactory
 {
 public IModelBinder CreateBinder(ModelBinderFactoryContext context)
 {
  .....
 }
 }

So how does this approach work internally? In fact, it is very simple. When we add the MVC framework, we start with 16 specific ModelBinderProviders, namely List < IModelBinderProvider>, and then go through the collection in this method. At this time, the internal implementation of the above method becomes pseudocode as follows:


public class ModelBinderFactory : IModelBinderFactory
 {
 public IModelBinder CreateBinder(ModelBinderFactoryContext context)
 {
  IModelBinderProvider[] _providers;
  IModelBinder result = null;

  for (var i = 0; i < _providers.Length; i++)
  {
  var provider = _providers[i];
  result = provider.GetBinder(providerContext);
  if (result != null)
  {
   break;
  }
  }
 }
 }

As for how to get which specific Model Binder Provider it is, it involves the implementation of specific details. Simply speaking, according to the Binding Source and the corresponding metadata information, if you want to see the details of the source code, you can download and enlarge the following image to see.

Custom Model Binding

After a brief introduction of the principle of model binding, more details can be seen from the above figure. Next, we start to practice. Through the above overall explanation, we know that in order to achieve custom model binding, we must implement two interfaces, IModelBinderProvider interface to instantiate Model Binder and IModelBinder. Interface to bind parameters, and finally, add our custom implemented ModelBinderProvider to the ModelBinderProvider collection in the MVC framework options. First, we define the following classes:


public class Employee
 {
 [Required]
 public decimal Salary { get; set; }
 }

We define a class of employees, employees have salaries, if the company is spread all over the world, so for different currencies in different countries, assuming that Chinese employees, then the currency is RMB, assuming that a Chinese employee’s salary is RMB 10,000, we want to tie [10,000] to the Salary attribute, at this time through Po. Look at the stman mock request.


[Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post(Employee customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }

As can be seen from the response of the above figure, the default model binding system will no longer be applicable at this time, because we have added currency symbols, so we must implement custom model binding at this time. Next, we implement custom model binding in two different ways.

Binding Method of Custom Model for Money Symbols (I)

We know that money symbols can be specified by NumberStyles. Currency. Children’s shoes who have understood the principle of model binding should know that there is a Decimal Model BinderProvider in the default set of Model BinderProviders of. NET Core, but a Floating Point Type Model BinderProvider to support money symbols, and the corresponding behind. The specific implementation is DecimalModel Binder, so we can use the built-in DecimalModel Binder to achieve custom model binding, so at this time we only need to implement the IModelBinderProvider interface, and the IModelBinder interface corresponds to the built-in DecimalModel Binder has been implemented, the code is as follows:

public class RMBModelBinderProvider : IModelBinderProvider
 {
 private readonly ILoggerFactory _loggerFactory;
 public RMBModelBinderProvider(ILoggerFactory loggerFactory)
 {
  _loggerFactory = loggerFactory;

 }
 public IModelBinder GetBinder(ModelBinderProviderContext context)
 {
  // Metadata skips directly for complex types
  if (context.Metadata.IsComplexType)
  {
  return null;
  }

  // Getting metadata types in context directly skips over non-decimal types
  if (context.Metadata.ModelType != typeof(decimal))
  {
  return null;
  }
  
  return new DecimalModelBinder(NumberStyles.Currency, _loggerFactory);
 }
 }

Next we add the RMBModelBinderProvider we implemented above to the ModelBinderProviders collection. Here we need to note that we know that we will eventually get a specific ModelBinder. The built-in is implemented by traversing the collection. Once we find a direct jump-out, we will customize the implementation of the ModelBinderProvider strongly. It is recommended that the Insert method, rather than the Add method, be added to the first place in the collection, as follows:


services.AddMvc(options =>
  {
  var loggerFactory = _serviceProvider.GetService<ILoggerFactory>();
  options.ModelBinderProviders.Insert(0, new RMBModelBinderProvider(loggerFactory));
  }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Binding Method of Custom Model for Money Symbols (2)

The problem of currency symbols is solved by using the DecimalModel Binder built-in to us. Next, we will implement the designated attribute as currency symbols through the characteristics. First, we define the following interface to analyze whether the attribute value is successful or not.


public interface IRMB
 {
 decimal RMB(string modelValue, out bool success);
 }

Then write the following RMB attribute features to implement the above interface.


[AttributeUsage(AttributeTargets.Property)]
 public class RMBAttribute : Attribute, IRMB
 {
 private static NumberStyles styles = NumberStyles.Currency;
 private CultureInfo CultureInfo = new CultureInfo("zh-cn");
 public decimal RMB(string modelValue, out bool success)
 {
  success = decimal.TryParse(modelValue, styles, CultureInfo, out var valueDecimal);
  return valueDecimal;
 }
 }

Next, we implement the IModelBinderProvider interface, and then in this interface implementation to obtain whether the properties in the model metadata type implement the above RMB features. If so, we instantiate the Model Binder and pass the RMB features to the past and get their values. The complete code is as follows:


public class RMBAttributeModelBinderProvider : IModelBinderProvider
 {
 private readonly ILoggerFactory _loggerFactory;
 public RMBAttributeModelBinderProvider(ILoggerFactory loggerFactory)
 {
  _loggerFactory = loggerFactory;

 }
 public IModelBinder GetBinder(ModelBinderProviderContext context)
 {
  if (!context.Metadata.IsComplexType)
  {
  var propertyName = context.Metadata.PropertyName;
  var propertyInfo = context.Metadata.ContainerMetadata.ModelType.GetProperty(propertyName);
  var attribute = propertyInfo.GetCustomAttributes(typeof(RMBAttribute), false).FirstOrDefault();
  if (attribute != null)
  {
   return new RMBAttributeModelBinder(context.Metadata.ModelType, attribute as RMBAttribute, _loggerFactory);
  }
  }
  return null;
 }
 }

public class RMBAttributeModelBinder : IModelBinder
 {
 IRMB rMB;
 private SimpleTypeModelBinder modelBinder;
 public RMBAttributeModelBinder(Type type, RMBAttribute attribute, ILoggerFactory loggerFactory)
 {
  rMB = attribute as IRMB;
  modelBinder = new SimpleTypeModelBinder(type, loggerFactory);
 }
 public Task BindModelAsync(ModelBindingContext bindingContext)
 {
  var modelName = bindingContext.ModelName;
  var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
  if (valueProviderResult != ValueProviderResult.None)
  {
  bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
  var valueString = valueProviderResult.FirstValue;
  var result = rMB.RMB(valueString, out bool success);
  if (success)
  {
   bindingContext.Result = ModelBindingResult.Success(result);
   return Task.CompletedTask;
  }
  }
  return modelBinder.BindModelAsync(bindingContext);
 }
 }

Finally, add to the collection and use RMB features on attribute Salary, such as what the ModelBinderContext and ModelBinderProviderContext contexts are, just model metadata and some parameters. Here we will not explain them one by one, and we will know more about debugging ourselves. As follows:


 services.AddMvc(options =>
 {
  var loggerFactory = _serviceProvider.GetService<ILoggerFactory>();
  options.ModelBinderProviders.Insert(0, new RMBAttributeModelBinderProvider(loggerFactory));
 }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

public class Employee
 {
 [Required]
 [RMB]
 public decimal Salary { get; set; }
 }

Mixed binding

What is hybrid binding? Let me give you an example, for example, if we want to bind the parameters on the URL to the parameters of the FromBody feature, provided that the parameters on the URL are not in the FromBody parameter, it still seems a bit modular. Muddy, come on, code.


[Route("[controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost("{id:int}")]
 public IActionResult Post([FromBody]Employee customer)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

public class Employee
 {
 public int Id { get; set; }
 [Required]
 public decimal Salary { get; set; }
 }

As the sketch above must have been clear, we do not specify attribute Id in Body, but we want to bind the ID in the routing to attribute Id of Employee, the parameter identified by FromBody. The example is not reasonable, just to demonstrate hybrid binding, which should be ignored. The problem has been explained very clearly. I wonder if you have any idea to solve it. Since it is FromBody, we still need to bind the built-in implemented BodyModel Binder. We only need to bind the value in the routing to the ID in the Employee object. First, we implement the IModelBinderProvider interface, as follows:

public class MixModelBinderProvider : IModelBinderProvider
 {
 private readonly IList<IInputFormatter> _formatters;
 private readonly IHttpRequestStreamReaderFactory _readerFactory;

 public MixModelBinderProvider(IList<IInputFormatter> formatters,
  IHttpRequestStreamReaderFactory readerFactory)
 {
  _formatters = formatters;
  _readerFactory = readerFactory;
 }
 public IModelBinder GetBinder(ModelBinderProviderContext context)
 {
  // If the context is empty, return empty
  if (context == null)
  {
  throw new ArgumentNullException(nameof(context));
  }

  // If the metadata model type is Employee, instantiate MixModel Binder
  if (context.Metadata.ModelType == typeof(Employee))
  {
  return new MixModelBinder(_formatters, _readerFactory);
  }

  return null;
 }
 }

Next is the implementation of the IModelBinder interface, binding the [FromBody] feature request parameters, and binding the property Id.

public class MixModelBinder : IModelBinder
 {
 private readonly BodyModelBinder bodyModelBinder;
 public MixModelBinder(IList<IInputFormatter> formatters,
  IHttpRequestStreamReaderFactory readerFactory)
 {
  // The original [FromBody] binding parameter is still bound, so you need to instantiate the BodyModel Binder
  bodyModelBinder = new BodyModelBinder(formatters, readerFactory);
 }
 public Task BindModelAsync(ModelBindingContext bindingContext)
 {
  if (bindingContext == null)
  {
  throw new ArgumentNullException(nameof(bindingContext));
  }

  // Binding [FromBody] feature request parameters
  bodyModelBinder.BindModelAsync(bindingContext);

  if (!bindingContext.Result.IsModelSet)
  {
  return null;
  }

  // Get the bound object
  var model = bindingContext.Result.Model;

  // Binding property Id
  if (model is Employee employee)
  {
  var idString = bindingContext.ValueProvider.GetValue("id").FirstValue;
  if (int.TryParse(idString, out var id))
  {
   employee.Id = id;
  }

  bindingContext.Result = ModelBindingResult.Success(model);
  }
  return Task.CompletedTask;
 }
 }

In fact, we should be more aware that the BindRequired and BindNever features only work for the MVC model binding system, and the request parameters for the FromBody feature are related to the Input Formatter, that is, to the serialization and deserialization frameworks used. Next, we add a custom implementation of the hybrid binding class, as follows:


services.AddMvc(options =>
  {
  var readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
  options.ModelBinderProviders.Insert(0, new MixModelBinderProvider(options.InputFormatters, readerFactory));
  }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Essence of ApiController Characteristics

Every iteration update of. NET Core gives us the best experience. Until. NET Core 2.0, we knew that MVC and Web Api merged controllers and inherited them from Controller. But after all, if we only did Api development, we would not use Razor view engine in MVC at all. In. NET Core 2.1, Ap appeared. The iController feature, along with new conventions, is that our controller base class can no longer be Controller but Controller Base, which is a lighter controller base class. It does not support Razor View Engine, Controller Base Controller and API Controller features, and completely evolves into clean Api control. Controller, so at least here we know the difference between Controller and Controller Base in. NET Core. Controller includes Razor view engine, and if we only do interface development, we only need to use Controller Base controller to combine ApiController features. So the question is, what exactly does the ApiController feature bring us? More specifically, what problems does it solve for us? Some people say that the emergence of model binding system or ApiController feature in. NET Core is very complex, but in fact, we do not understand the application scenario behind it. Once used, we find that a variety of problems have emerged, or the foundation has not been consolidated. Let’s take a look next. In explaining the model binding system, we learned that validation of parameters needs to be judged by code ModelState. IsValid, such as the following code:


public class Employee
 {
 public int Id { get; set; }

 [Required]
 public string Address { get; set; }
 }

 [Route("[Controller]")]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post([FromBody]Employee employee)
 {
  if (!ModelState.IsValid)
  {
  return BadRequest(ModelState);
  }
  return Ok();
 }
 }

When the Address attribute is not included in the request parameter, the response 400 is not validated by the above model. When the controller is modified by ApiController, the built-in will automatically verify. That is, we do not need to write the ModelState. IsValid method over and over in the controller method. Then the question arises. How does the built-in automatically verify? First, when the. NET Core application is initialized, the following interfaces and specific implementation will be injected.


services.TryAddEnumerable(
  ServiceDescriptor.Transient<IApplicationModelProvider, ApiBehaviorApplicationModelProvider>());

So what exactly has been done with the class ApiBehavior Application Model Provider? Six conventions have been added to this constructor. The other four are not the focus of our study. Interested children’s shoes can be studied privately. Let’s look at the two most important categories: Invalid Model State Filter Convention and InferParameter Binding InfoConvention. Then there are the following methods in this class:


public void OnProvidersExecuting(ApplicationModelProviderContext context)
 {
  foreach (var controller in context.Result.Controllers)
  {
  if (!IsApiController(controller))
  {
   continue;
  }

  foreach (var action in controller.Actions)
  {
   // Ensure ApiController is set up correctly
   EnsureActionIsAttributeRouted(action);

   foreach (var convention in ActionModelConventions)
   {
   convention.Apply(action);
   }
  }
  }
 }

As for when the OnProvider Executing method is called, we don’t need much concern. This is not the focus of our research. We can see that the specific point of this method is to judge whether we modify the controller through ApiController or not. If so, we can go through the six conventions we added by default. Let’s first look at the InvalidModelStateFilterConvention Convention convention, and finally we see that a ModelStateInvalidFilterFactory has been added to this class. Then we instantiate the ModelStateInvalidFilter class for this class. Then we see the implementation of the IAactionFilter interface in this class, as follows:


public void OnActionExecuting(ActionExecutingContext context)
 {
  if (context.Result == null && !context.ModelState.IsValid)
  {
  _logger.ModelStateInvalidFilterExecuting();
  context.Result = _apiBehaviorOptions.InvalidModelStateResponseFactory(context);
  }
 }

By now we must have understood that the first problem was solved on the controller by ApiController modification:When we add the MVC framework, we inject a ModelStateInvalidFilter, which runs during the OnAction Executing method, that is, when we execute the controller method, and automatically verify the validity of the ModelState after model binding. If we fail, we immediately respond to 400.Is this the end of it? Obviously not. Why, we modify the controller with ApiController, as follows:


[Route("[Controller]")]
 [ApiController]
 public class ModelBindController : Controller
 {
 [HttpPost]
 public IActionResult Post(Employee employee)
 {
  //if (!ModelState.IsValid)
  //{
  // return BadRequest(ModelState);
  //}
  return Ok();
 }
 }

Comparing with the above code, we just add ApiController to modify the controller, and we have already done model validation internally, so we annotate the model validation code, and then we remove the FromBody feature. At this time, we make a request and respond as follows, which meets our expectations:

We’re just adding ApiController modifiers to the controller. Why do we remove the FromBody feature and the request is still good, and the result is the same as we expected? The answer is: parameter source binding inference. By modifying the controller with ApiController, we will use the second convention class (parameter binding information inference) we mentioned above. Did we find out that. NET Core has done a lot for us? Don’t worry, things have not yet come to an end. Let’s take a look at it. The example of binding URL parameters to dictionaries given earlier.


[Route("[Controller]")]
 [ApiController]
 public class ModelBindController : Controller
 {
 [HttpGet]
 public IActionResult Get(List<Dictionary<string, int>> pairs)
 {
  return Ok();
 }
 }

Here we are momentarily confused, the previous request now appears 415, that is, the media type does not support, we did nothing, but added ApiController modifier controller, that is, the problem has a 180 degree turning point, who will explain this question. Let’s take a look at the implementation of the parameter binding information Convention class.


if (!options.SuppressInferBindingSourcesForParameters)
  {
  var convention = new InferParameterBindingInfoConvention(modelMetadataProvider)
  {
   AllowInferringBindingSourceForCollectionTypesAsFromQuery = options.AllowInferringBindingSourceForCollectionTypesAsFromQuery,
  };

  ActionModelConventions.Add(convention);
  }

The first judgment is whether to start the parameter source binding inference, telling us that this is configurable, well, we will restore it not enabled, then request the return as before, as follows:


services.Configure<ApiBehaviorOptions>(options=>
 {
 options.SuppressInferBindingSourcesForParameters = true;
 }).AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

So what does the built-in do? Actually, the answer is given. Let’s look at the above line of code: options. AllowInferring Binding Source ForCollection Types As FromQuery. Because for collection type, it’s impossible to infer whether. NET Core comes from Body or Query, so. NET Core gives us one more. There are three configurable options. We explicitly configure that the collection type is from Query by configuring it as follows. The request is good at this time. Otherwise, it will default to Body, so 415 appears.


services.Configure<ApiBehaviorOptions>(options=>
{
 options.AllowInferringBindingSourceForCollectionTypesAsFromQuery = true;
}).AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Well, the above is configurable and mandatory for the collection type to specify its source, so the question arises again. What about the object? First of all, we will derive the above explicit configuration set type from Query disabled (disabled or not disabled). Let’s look at the following code:


[Route("[Controller]")]
 [ApiController]
 public class ModelBindController : Controller
 {
 [HttpGet("GetEmployee")]
 public IActionResult GetEmployee(Employee employee)
 {
  return Ok();
 }
 }

Again, let’s be shocked. It seems that since the addition of ApiController modifier controller, various problems have arisen. Let’s look at. NET Core’s final inference. How on earth is it inferred?


internal void InferParameterBindingSources(ActionModel action)
 {
  for (var i = 0; i < action.Parameters.Count; i++)
  {
  var parameter = action.Parameters[i];
  var bindingSource = parameter.BindingInfo?.BindingSource;
  if (bindingSource == null)
  {
   bindingSource = InferBindingSourceForParameter(parameter);

   parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
   parameter.BindingInfo.BindingSource = bindingSource;
  }
  }
  ......
 }

 // Internal for unit testing.
 internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
 {
  if (IsComplexTypeParameter(parameter))
  {
  return BindingSource.Body;
  }

  if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
  {
  return BindingSource.Path;
  }

  return BindingSource.Query;
 }

 private bool ParameterExistsInAnyRoute(ActionModel action, string parameterName)
 {
  foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(action))
  {
  if (route == null)
  {
   continue;
  }

  var parsedTemplate = TemplateParser.Parse(route.Template);
  if (parsedTemplate.GetParameter(parameterName) != null)
  {
   return true;
  }
  }

  return false;
 }

 private bool IsComplexTypeParameter(ParameterModel parameter)
 {
  // No need for information from attributes on the parameter. Just use its type.
  var metadata = _modelMetadataProvider
  .GetMetadataForType(parameter.ParameterInfo.ParameterType);

  if (AllowInferringBindingSourceForCollectionTypesAsFromQuery && metadata.IsCollectionType)
  {
  return false;
  }

  return metadata.IsComplexType;
 }

From the above code, we can see that there are only three kinds of inference results: Body, Path, Query. Because we do not explicitly configure the binding source, we infer the source by parameters, and then first determine whether it is a complex type, if Allow Inferring Binding Source ForCollection Types As FromQuery is configured to true, and indicate the source for the collection type as Body. Whether or not we explicitly configure the binding set type from FromQuery or not, we certainly do not satisfy these two conditions. Then we execute metadate. IsComplexType. It is obvious that Employee is a complex type. Once again, we can prove by source code that when we obtain model metadata, we can use TypeDescriptor. GetConverter (Mo typeof). CanConvertFrom (typeof (string)) determines whether a complex type is present, so the return binding originates from the Body at this point, so 415 appears. The problem has been analyzed clearly. Come on, finally, let’s draw a conclusion about the nature of the ApiController feature:

By modifying the controller with ApiController, six default conventions are implemented. The two most important ones are: one is to solve the problem of automatic model validation; the other is to infer the source of execution parameters when the binding source is not configured. However, this is only for Body, Path and Query.

When the parameters on the controller method are dictionaries or collections, if the request parameters are from the URL, that is, the query string, explicitly configure AllowInferring Binding Source ForCollection Types As FromQuery to be true, otherwise the binding source will be inferred to be Body, thus responding to 415.

When the parameters on the controller method are complex types, if the request parameters are from Body, there is no need to explicitly configure the binding source. If the parameters are from the URL, that is, the query string, please explicitly configure the parameter binding source [FromQuery]. If the parameters are from the form, please explicitly configure the parameter binding source [FromForm]. 】 Otherwise, the binding is inferred to be Body, which responds to 415.

summary

In this paper, the model binding system, model binding principle, custom model binding principle, hybrid binding and so on in. NET Core are described in detail. In fact, there are some basic contents that I haven’t written yet. It is possible that I will study and supplement them later. The powerful model binding support and flexibility control in. NET Core are. NET. MVC/Web Api is incomparable, although very basic, but how many people know and understand these, and for the ApiController features do save us unnecessary code, but the parameter source inference brought us a bit confused, if you do not look at the source code, you certainly do not know these, I personally think for add. After adding the ApiController feature, the parameter source inference is useless. It is strongly recommended to explicitly configure the binding source, so it is not necessary to remember the above conclusion. This article took me three days to write and patch, and the value it brings is a word: value.