- 1. Writing a Dependency Injection Framework
- 2. Writing Controllers and Parameter Types
- 3. Implementing a Low-Cost Imitation of ASP.NET Core
Starting from the fourth section, we entered the practical exercises; the fifth section implemented the instantiation of a type and the invocation of member methods, among other operations, which will be introduced in later chapters.
Since this series focuses on practical exercises, there may be many articles with lengthy content. The best way to learn a technology is to follow an example and write the code once, running and debugging it.
This article serves as a stage exercise, summarizing all the knowledge points learned previously and implementing a dependency injection feature akin to ASP.NET Core, accessing APIs, automatically passing parameters and executing methods, and finally returning results.
The code for this chapter has been uploaded to https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射与特性(6)设计一个仿ASP.NET%20Core依赖注入框架.cs
Effect:
User Effects
- The user can access the Controller
- The user can access the Action
- Parameters are passed when accessing the Action
Program Requirements Effects
- Instantiate types
- Identify type constructor types
- Dynamically instantiate types based on constructor types and inject dependencies
- Dynamically invoke the appropriate overloaded method
1. Writing a Dependency Injection Framework
The code after completion is roughly like this
The author wrote it directly in the Program class, with about 200 lines of code (including detailed comments, blank lines).
Before writing the code, please import the following namespaces:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Add the following code in Program
private static Assembly assembly = Assembly.GetCallingAssembly();
private static Type[] types;
static Program()
{
types = assembly.GetTypes();
}
The purpose of the above code is to obtain the current program's assembly and metadata information.
This is the first step of reflection.
1.1 Routing Index
The routing rules in ASP.NET Core are very rich, allowing for the customization of various URL rules. The main principle is that during runtime, the program collects features such as [route] of Controllers and Actions to generate a routing table.
The execution basis of the program is types and methods, where the Controller in ASP.NET Core is a Class, and Action is a Method.
From our previous studies, we learned that to instantiate and call a member of a type via reflection, it is sufficient to confirm the type name and method name.
For the routing table, we can assume (not referring to ASP.NET Core's principle) that when a user accesses a URL, the program first compares it against the routing table; if there is a match, it retrieves the corresponding Class and Method, which are then invoked via reflection.
We will not implement this complex structure, but simply realize routing at the Controller-Action level.
1.1.1 Checking if the Controller exists
In the Program, add a method to check whether this controller exists in the current assembly.
/// <summary>
/// Check if the controller exists and return Type
/// </summary>
/// <param name="controllerName">Controller name (without 'Controller')</param>
/// <returns></returns>
private static (bool, Type) IsHasController(string controllerName)
{
// Case insensitive
string name = controllerName + "Controller";
if (!types.Any(x => x.Name.ToLower() == name.ToLower()))
return (false, null);
return (true, types.FirstOrDefault(x => x.Name.ToLower() == name.ToLower()));
}</code></pre>
The code is quite simple, and with the help of Linq, just a few lines accomplishes the task.
Implementation principle:
Check if there is a type with the name {var}Controller
in the assembly, for example, HomeController
.
If it exists, retrieve the Type of that controller.
1.1.2 Checking if the Action exists
Action is within the Controller (methods within a type), so we only need to check the following.
/// <summary>
/// Check if a controller has this method
/// </summary>
/// <param name="type">Controller type</param>
/// <param name="actionName">Action name</param>
/// <returns></returns>
private static bool IsHasAction(Type type, string actionName)
{
// Case insensitive
return type.GetMethods().Any(x => x.Name.ToLower() == actionName.ToLower());
}</code></pre>
Implementation principle:
Check if a method actionname
exists within a type.
Here, we do not return MethodInfo
, but rather return bool
, considering that methods can be overloaded, and we need to determine which method to use based on the parameters provided during the request.
Thus, we only perform checks; the process for obtaining the MethodInfo
will come later.
1.2 Dependency Instantiation
This means obtaining all parameter information in a type's constructor and automatically creating instances for each type.
Input Parameter:
The Type of the type to be injected with dependencies.
Return Data:
A list of instances for constructor parameters (reflection always results in objects).
/// <summary>
/// Instantiate dependencies
/// </summary>
/// <param name="type">The type to be instantiated and injected</param>
public static object[] CreateType(Type type)
{
// Only use one constructor
ConstructorInfo construct = type.GetConstructors().FirstOrDefault();
// Get constructor parameters of the type
ParameterInfo[] paramList = construct.GetParameters();
// List of objects for dependency injection
List<object> objectList = new List<object>();
// Instantiate a type for each parameter type of the constructor
foreach (ParameterInfo item in paramList)
{
// Get parameter type: item.ParameterType.Name
// Find which type in the program implements item’s interface
Type who = types.FirstOrDefault(x => x.GetInterfaces().Any(z => z.Name == item.ParameterType.Name));
// Instantiate
object create = Activator.CreateInstance(who, new object[] { });
objectList.Add(create);
}
return objectList.ToArray();
}
Here are two points:
① A type may have multiple constructors;
② When writing a controller in ASP.NET Core, probably no one would write two constructors.
Based on these two points, we only need one constructor and do not need to consider many situations; we assume that a controller is only allowed to define one constructor, not multiple constructors.
Implementation principle:
After obtaining the constructor, retrieve the parameter list of the constructor (ParameterInfo[]
).
There are also several issues to consider:
-
Parameter is an interface type
-
Parameter is an abstract type
-
Parameter is a regular Class type
Thus, according to the above divisions, the situations to consider increase. Here we agree, based on the Dependency Inversion Principle, that types within constructors are only allowed to be interfaces.
Since there is no IOC container here, and we are merely implementing basic reflection, we do not need to consider many situations (what else do you want with 200 lines of code...).
Later, we will search for types that implement this interface and instantiate them to pass as parameters.
Note: More practical tutorials will be continuously released in the future; stay tuned, and you can follow the WeChat subscription account "NCC Open Source Community" for the latest news.
1.3 Instantiation of Type, Dependency Injection, Calling Method
We have now arrived at the final stage of dependency injection, instantiating a type, injecting dependencies, and invoking methods.
/// <summary>
/// Implement dependency injection and call a method
/// </summary>
/// <param name="type">Type</param>
/// <param name="actionName">Method name</param>
/// <param name="paramList">List of parameters for method invocation</param>
/// <returns></returns>
private static object StartASPNETCORE(Type type, string actionName, params object[] paramList)
{
// Get overloaded methods
// Same name, same number of parameters
MethodInfo method = type.GetMethods()
.FirstOrDefault(x => x.Name.ToLower() == actionName.ToLower()
&& x.GetParameters().Length == paramList.Length);
// If there are problems with the parameters, no suitable Action overload can be found
// Return 405
if (method == null)
return "405";
// Instantiate controller
// Get dependency objects
object[] inject = CreateType(type);
// Inject dependencies, instantiate the object
object example = Activator.CreateInstance(type, inject);
// Execute method and return execution result
object result;
try
{
result = method.Invoke(example, paramList);
return result;
}
catch
{
// Return 500
result = "500";
return result;
}
}</code></pre>
Implementation principle:
With the CreateType
method, we have already obtained the parameter objects for the instantiated type's constructor.
The method for determining which overloaded method to call is through the number of parameters, since console input can only retrieve string
; for more complex scenarios where parameter types are involved in retrieving overloaded methods, users may explore further independently.
Calling a method typically involves the following steps (in no specific order):
Get instance of type;
Get Type of the instance;
Get MethodInfo for the method;
Obtain parameter objects for the method;
// Get dependency objects
object[] inject = CreateType(type);
// Inject dependencies, instantiate the object
object example = Activator.CreateInstance(type, inject);
The above code implements a very simple dependency injection process.
The remaining task is to invoke the method based on the number of parameters and call the corresponding overloaded method.
2. Writing Controllers and Parameter Types
2.1 Writing Types
Define an interface
/// <summary>
/// Interface
/// </summary>
public interface ITest
{
string Add(string a, string b);
}
Implement the interface
/// <summary>
/// Implementation
/// </summary>
public class Test : ITest
{
public string Add(string a, string b)
{
Console.WriteLine("Add method executed");
return a + b;
}
}
2.2 Implementing Controller
We will write a controller that resembles the typical form in ASP.NET Core, effectively creating a low imitation controller.
/// <summary>
/// Class that requires automatic instantiation and dependency injection
/// </summary>
public class MyClassController
{
private ITest _test;
public MyClassController(ITest test)
{
_test = test;
}
/// <summary>
/// This is an Action
/// </summary>
/// <returns></returns>
public string Action(string a, string b)
{
// Validate HTTP request parameters
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b))
return "Validation failed";
// Start execution
var result = _test.Add(a, b);
Console.WriteLine("NCC community", "Awesome");
// Respond with result
return result;
}
}</code></pre>
This is a common scenario for using dependency injection:
private ITest _test;
public MyClassController(ITest test)
{
_test = test;
}
This can be a database context or various types.
Since the console input retrieves string
, to simplify matters, only Action
methods with string
parameter types are used.
3. Implementing a Low-Cost Imitation of ASP.NET Core
Okay, I admit this has nothing to do with ASP.NET Core; this is a very simple feature.
The main aim is to imitate StartUp, implementing the request flow and data return.
.
static void Main(string[] args)
{
while (true)
{
string read = string.Empty;
Console.WriteLine("Hello user, please enter the controller you want to access (do not include 'Controller')");
read = Console.ReadLine();
// Check if this controller exists and get its Type
var hasController = IsHasController(read);
// If the controller is not found, return 404 and ask the user to request again
if (!hasController.Item1)
{
Console.WriteLine("404");
continue;
}
Console.WriteLine("The controller exists, please continue by entering the action you want to access");
read = Console.ReadLine();
// Check if this Action exists and get its Type
bool hasAction = IsHasAction(hasController.Item2, read);
// If it's not found, continue to return 404
if (hasAction == false)
{
Console.WriteLine("404");
continue;
}
// Up to this point, the URL exists, so now we need to pass parameters
Console.WriteLine("Hello user, the URL exists, please input parameters");
Console.WriteLine("Enter each parameter and press enter, to end input please enter 0 and then press enter");
// Start receiving user input parameters
List<object> paramList = new List<object>();
while (true)
{
string param = Console.ReadLine();
if (param == "0")
break;
paramList.Add(param);
}
Console.WriteLine("Input ended, sending HTTP request \n");
// The user's request has been validated and started, now we proceed to simulate ASP.NET Core execution
object response = StartASPNETCORE(hasController.Item2, read, paramList.ToArray());
Console.WriteLine("The result of execution is:");
Console.WriteLine(response);
Console.ReadKey();
}</code></pre>
Implementation Process and Principles:
- Determine whether the URL exists (routing)
- Receive user input parameters
- Implement dependency injection
- Invoke method, transfer parameters, return execution result