C# AOP Learning Notes

Posted by Shandrio on Tue, 17 Mar 2020 17:54:29 +0100

1. AOP Concepts

Official Interpretation: AOP (Aspect-Oriented Programming) is a technology that dynamically and uniformly adds functionality to a program without modifying its source code through precompilation and run-time dynamic proxies.It is a new methodology and a complement to traditional OOP programming.OOP is concerned with dividing requirement functions into different, relatively independent, well-encapsulated classes and letting them have their own behavior, relying on Inheritance and polymorphism to define their relationship. AOP is the hope that it can separate common requirement functions from unrelated classes and enable many classes to share a single behavior without modifying many classes as changes occur.You just need to modify this behavior.AOP uses aspects to modularize cross-cutting concerns, and OOP uses classes to modularize States and behaviors.In the world of OOP, programs are organized through classes and interfaces, and it is appropriate to use them to implement the core business logic of a program, but it is very difficult to achieve cross-cutting concerns (functional requirements that span multiple modules of an application), such as logging, privilege validation, exception interception, and so on.

Personal understanding: AOP is to extract the public functions, if the requirements of public functions change in the future, you only need to change the code of the public module, and there is no need to change where multiple calls are made.Face-oriented means focusing only on common functions, not on business logic.It typically works by intercepting, for example, projects that typically have privilege validation capabilities that verify whether the current logged-in user has permission to view the interface before entering each page.It is impossible to say that this validation code is written in the initialization method of each page, and our AOP will come in handy at this time.The mechanism of AOP is to predefine a set of attributes so that it has the ability to intercept methods that allow you to do what you want before and after executing a method, whereas all we need to do is add a feature to the corresponding method or class definition.

2. AOP Advantage

1) By separating common functions from business logic, a large number of duplicate codes can be omitted, which is conducive to code operation and maintenance.

2) In software design, extract common functions (facets) to facilitate the modularization of software design and reduce the complexity of software architecture.That is to say, a common function is a separate module, the design code of these common functions can not be seen in the main business of the project.

3. AOP Application

3.1. Static proxy mode

3.1.1. Implement static proxy using decorator mode

1) Create a new class: DecoratorAOP.cs

    /// <summary>
    /// Implement static proxy using decorator mode
    /// </summary>
    public class DecoratorAOP
    {
        /// <summary>
        /// User class
        /// </summary>
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Password { get; set; }
        }

        /// <summary>
        /// User Registration Interface
        /// </summary>
        public interface IUserProcessor
        {
            void RegUser(User user);
        }

        /// <summary>
        /// User Registration Interface Implementation Class
        /// </summary>
        public class UserProcessor : IUserProcessor
        {
            public void RegUser(User user)
            {
                Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
            }
        }

        /// <summary>
        /// Decorator Mode Implementation AOP function
        /// </summary>
        public class UserProcessorDecorator : IUserProcessor
        {
            private IUserProcessor UserProcessor { get; set; }
            public UserProcessorDecorator(IUserProcessor userProcessor)
            {
                UserProcessor = userProcessor;
            }

            public void RegUser(User user)
            {
                PreProceed(user);
                UserProcessor.RegUser(user);
                PostProceed(user);
            }

            public void PreProceed(User user)
            {
                Console.WriteLine("Before method execution");
            }

            public void PostProceed(User user)
            {
                Console.WriteLine("After method execution");
            }
        }

        /// <summary>
        /// Run tests
        /// </summary>
        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "World" };
            IUserProcessor processor = new UserProcessorDecorator(new UserProcessor());
            processor.RegUser(user);
        }
    }

2) Call:

        static void Main(string[] args)
        {
            #region Implement static proxy using decorator mode
            DecoratorAOP.Show();
            Console.Read();
            #endregion
        }

3) The results are as follows:

The above code is an example of simulated user registration: before submitting registration information, you need to do some preparatory work, such as data validity check, and after submitting registration information, you need to do some logging.From the code above, we can see that we manually let the method do what we need before and after it is executed by static seeding.

3.1.2. Implementing static proxy using proxy mode

1) Create a new class: ProxyAOP.cs

    /// <summary>
    /// Implement static proxy using proxy mode
    /// </summary>
    public class ProxyAOP
    {
        /// <summary>
        /// User class
        /// </summary>
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Password { get; set; }
        }

        /// <summary>
        /// User Registration Interface
        /// </summary>
        public interface IUserProcessor
        {
            void RegUser(User user);
        }

        /// <summary>
        /// User Registration Interface Implementation Class
        /// </summary>
        public class UserProcessor : IUserProcessor
        {
            public void RegUser(User user)
            {
                Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
            }
        }

        /// <summary>
        /// Proxy Mode Implementation AOP function
        /// </summary>
        public class UserProcessorProxy : IUserProcessor
        {
            private IUserProcessor userProcessor = new UserProcessor();

            public void RegUser(User user)
            {
                PreProceed(user);
                userProcessor.RegUser(user);
                PostProceed(user);
            }

            private void PreProceed(User user)
            {
                Console.WriteLine("Before method execution");
            }

            private void PostProceed(User user)
            {
                Console.WriteLine("After method execution");
            }
        }

        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "World" };
            IUserProcessor processor = new UserProcessorProxy();
            processor.RegUser(user);
        }
    }

2) Call:

        static void Main(string[] args)
        {
            #region Implement static proxy using proxy mode
            ProxyAOP.Show();
            Console.Read();
            #endregion
        }

3) The results are as follows:

3.2. Dynamic Agent Method

3.2.1. Implement dynamic proxy using.Net Remoting/RealProxy

1) Create a new class: RealProxyAOP.cs

    /// <summary>
    /// Use.Net Remoting/RealProxy Implement dynamic proxy
    /// Client - TransparentProxy - RealProxy - Target Object
    /// Limited to business class must be inherited from MarshalByRefObject type
    /// </summary>
    public class RealProxyAOP
    {
        /// <summary>
        /// User class
        /// </summary>
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Password { get; set; }
        }

        /// <summary>
        /// User Registration Interface
        /// </summary>
        public interface IUserProcessor
        {
            void RegUser(User user);
        }

        /// <summary>
        /// User Registration Interface Implementation Class
        /// Must inherit from MarshalByRefObject Parent class, otherwise it cannot be generated.
        /// </summary>
        public class UserProcessor : MarshalByRefObject, IUserProcessor
        {
            public void RegUser(User user)
            {
                Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
            }
        }

        /// <summary>
        /// Real Agent: Provides the basic functions of an agent
        /// </summary>
        public class MyRealProxy<T> : RealProxy
        {
            private T _target;
            public MyRealProxy(T target) : base(typeof(T))
            {
                _target = target;
            }

            public override IMessage Invoke(IMessage msg)
            {
                PreProceed(msg);
                IMethodCallMessage callMessage = (IMethodCallMessage)msg;
                object returnValue = callMessage.MethodBase.Invoke(_target, callMessage.Args);
                PostProceed(msg);
                return new ReturnMessage(returnValue, new object[0], 0, null, callMessage);
            }

            public void PreProceed(IMessage msg)
            {
                Console.WriteLine("Before method execution");
            }

            public void PostProceed(IMessage msg)
            {
                Console.WriteLine("After method execution");
            }
        }

        /// <summary>
        /// Transparent Proxy: Provides the illusion that the actual object resides in client space
        /// </summary>
        public static class TransparentProxy
        {
            public static T Create<T>()
            {
                T instance = Activator.CreateInstance<T>();
                MyRealProxy<T> realProxy = new MyRealProxy<T>(instance);
                T transparentProxy = (T)realProxy.GetTransparentProxy();
                return transparentProxy;
            }
        }

        /// <summary>
        /// Run tests
        /// </summary>
        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "World" };
            UserProcessor processor = TransparentProxy.Create<UserProcessor>();
            processor.RegUser(user);
        }
    }

2) Call:

        static void Main(string[] args)
        {
            #region Use.Net Remoting/RealProxy Implement dynamic proxy
            RealProxyAOP.Show();
            Console.Read();
            #endregion
        }

3) The results are as follows:

3.2.2. Using Castle\DynamicProxy to implement dynamic proxy

1) Install Castle.Core in NuGet.

2) Create a new class: CastleProxyAOP.cs

    /// <summary>
    /// Use Castle\DynamicProxy Implement dynamic proxy
    /// Method must be virtual
    /// </summary>
    public class CastleProxyAOP
    {
        /// <summary>
        /// User class
        /// </summary>
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Password { get; set; }
        }

        /// <summary>
        /// User Registration Interface
        /// </summary>
        public interface IUserProcessor
        {
            void RegUser(User user);
        }

        /// <summary>
        /// User Registration Interface Implementation Class
        /// </summary>
        public class UserProcessor : IUserProcessor
        {
            /// <summary>
            /// You must bring it with you virtual,Otherwise, it is invalid.
            /// </summary>
            /// <param name="user"></param>
            public virtual void RegUser(User user)
            {
                Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
            }
        }

        /// <summary>
        /// Interceptor
        /// </summary>
        public class MyInterceptor : IInterceptor
        {
            public void Intercept(IInvocation invocation)
            {
                PreProceed(invocation);
                invocation.Proceed();
                PostProceed(invocation);
            }
            public void PreProceed(IInvocation invocation)
            {
                Console.WriteLine("Before method execution");
            }

            public void PostProceed(IInvocation invocation)
            {
                Console.WriteLine("After method execution");
            }
        }

        /// <summary>
        /// Run tests
        /// </summary>
        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "World" };
            ProxyGenerator generator = new ProxyGenerator();
            MyInterceptor interceptor = new MyInterceptor();
            UserProcessor userprocessor = generator.CreateClassProxy<UserProcessor>(interceptor);
            userprocessor.RegUser(user);
        }
    }

3) Call:

        static void Main(string[] args)
        {
            #region Use Castle\DynamicProxy Implement dynamic proxy
            CastleProxyAOP.Show();
            Console.Read();
            #endregion
        }

4) The results are as follows:

3.2.3, AOP implementation using EntLib\PIAB Unity (non-configurable)

1) Install Unity and Unity.Interception in NuGet.

2) Create a new class: UnityAOP.cs

    /// <summary>
    /// Use EntLib\PIAB Unity Implement dynamic proxy(Non-Configurable)
    /// </summary>
    public class UnityAOP
    {
        #region business
        /// <summary>
        /// User class
        /// </summary>
        public class User
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public string Password { get; set; }
        }

        /// <summary>
        /// User Registration Interface
        /// </summary>
        [ExceptionHandler(Order = 1)]
        [LogHandler(Order = 2)]
        [UserHandler(Order = 3)]
        [AfterLogHandler(Order = 5)]
        public interface IUserProcessor
        {
            void RegUser(User user);
        }

        /// <summary>
        /// User Registration Interface Implementation Class
        /// </summary>
        public class UserProcessor : IUserProcessor //Can not inherit MarshalByRefObject class
        {
            public void RegUser(User user)
            {
                Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
            }
        }
        #endregion business

        #region Characteristic
        /// <summary>
        /// Exception handling characteristics
        /// </summary>
        public class ExceptionHandlerAttribute : HandlerAttribute
        {
            public override ICallHandler CreateHandler(IUnityContainer container)
            {
                return new ExceptionHandler() { Order = Order };
            }
        }

        /// <summary>
        /// Log Processing Features
        /// </summary>
        public class LogHandlerAttribute : HandlerAttribute
        {
            public override ICallHandler CreateHandler(IUnityContainer container)
            {
                return new LogHandler() { Order = Order };
            }
        }

        /// <summary>
        /// User Information Features
        /// </summary>
        public class UserHandlerAttribute : HandlerAttribute
        {
            public override ICallHandler CreateHandler(IUnityContainer container)
            {
                ICallHandler handler = new UserHandler() { Order = Order };
                return handler;
            }
        }

        /// <summary>
        /// Subsequent Log Features
        /// </summary>
        public class AfterLogHandlerAttribute : HandlerAttribute
        {
            public override ICallHandler CreateHandler(IUnityContainer container)
            {
                return new AfterLogHandler() { Order = Order };
            }
        }
        #endregion Characteristic

        #region Behavior corresponding to an attribute
        public class ExceptionHandler : ICallHandler
        {
            public int Order { get; set; }
            public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
            {
                IMethodReturn methodReturn = getNext()(input, getNext);

                if (methodReturn.Exception == null)
                {
                    Console.WriteLine("ExceptionHandler:No exceptions");
                }
                else
                {
                    Console.WriteLine($"ExceptionHandler:An exception occurred:{methodReturn.Exception.Message}");
                }

                return methodReturn;
            }
        }

        public class LogHandler : ICallHandler
        {
            public int Order { get; set; }
            public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
            {
                User user = input.Inputs[0] as User;
                string message = string.Format($"Name:{user.Name} Password:{user.Password}");
                Console.WriteLine($"LogHandler:Logged. Message:{message}");

                IMethodReturn methodReturn = getNext()(input, getNext);
                return methodReturn;
            }
        }

        public class UserHandler : ICallHandler
        {
            public int Order { get; set; }
            public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
            {
                User user = input.Inputs[0] as User;

                if (user.Password.Length < 10)
                {
                    return input.CreateExceptionMethodReturn(new Exception("UserHandler:Password length cannot be less than 10 bits"));
                }

                //getNext()(input, getNext): A delegation after delegation means a multiple delegation.
                IMethodReturn methodReturn = getNext()(input, getNext);
                return methodReturn;
            }
        }

        public class AfterLogHandler : ICallHandler
        {
            public int Order { get; set; }
            public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
            {
                IMethodReturn methodReturn = getNext()(input, getNext);
                Console.WriteLine($"AfterLogHandler:Method Execution Results--{methodReturn.ReturnValue}");
                Console.WriteLine("AfterLogHandler:After method execution");

                return methodReturn;
            }
        }
        #endregion Behavior corresponding to an attribute

        /// <summary>
        /// Run tests
        /// </summary>
        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "HelloWorld" };

            IUnityContainer container = new UnityContainer();           //Declare a container
            container.AddNewExtension<Interception>()
                .RegisterType<IUserProcessor, UserProcessor>(new Interceptor<TransparentProxyInterceptor>(), new InterceptionBehavior<PolicyInjectionBehavior>());  //Explicit Interception
            IUserProcessor processor = container.Resolve<IUserProcessor>();
            processor.RegUser(user);                                    //call
        }
    }

3) Call:

        static void Main(string[] args)
        {
            #region Use EntLib\PIAB Unity Implement dynamic proxy(Non-Configurable)
            UnityAOP.Show();
            Console.Read();
            #endregion
        }

4) The results are as follows:

3.2.4, AOP with Configuration using EntLib\PIAB Unity

1) Continue installing Unity.Configuration, Unity.Interception.Configuration, and Newtonsoft.Json in NuGet.

2) The following classes are established:

    /// <summary>
    /// User class
    /// </summary>
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Password { get; set; }
    }
Entity.cs (User Entity Class)
    /// <summary>
    /// User Registration Interface
    /// </summary>
    public interface IUserProcessor
    {
        void RegUser(User user);
    }
IUserProcessor.cs (User Registration Interface)
    /// <summary>
    /// User Registration Interface Implementation Class
    /// </summary>
    public class UserProcessor : IUserProcessor
    {
        public void RegUser(User user)
        {
            Console.WriteLine($"User registration was successful. Name:{user.Name} Password:{user.Password}");
        }
    }
UserProcessor.cs (User Registration Interface Implementation Class)
    /// <summary>
    /// Use EntLib\PIAB Unity Implement dynamic proxy(With Configuration)
    /// </summary>
    public class UnityConfigAOP
    {
        public static void Show()
        {
            User user = new User() { Name = "Hello", Password = "HelloWorld" };

            //To configure UnityContainer
            IUnityContainer container = new UnityContainer();
            ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap
            {
                ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + @"UnityConfigAOP\Unity.Config")
            };
            Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
            UnityConfigurationSection configSection = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);
            configSection.Configure(container, "AOPContainer");
            
            IUserProcessor processor = container.Resolve<IUserProcessor>();
            processor.RegUser(user);
        }
    }
UnityConfigAOP.cs (Run Test)
    /// <summary>
    /// No feature required
    /// </summary>
    public class ExceptionBehavior : IInterceptionBehavior
    {
        public bool WillExecute
        {
            get { return true; }
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            IMethodReturn methodReturn = getNext()(input, getNext);

            Console.WriteLine("ExceptionBehavior");
            if (methodReturn.Exception == null)
            {
                Console.WriteLine("No exception");
            }
            else
            {
                Console.WriteLine($"abnormal:{methodReturn.Exception.Message}");
            }
            return methodReturn;
        }
    }
ExceptionBehavior.cs (exception handling class)
    /// <summary>
    /// No feature required
    /// </summary>
    public class CachingBehavior : IInterceptionBehavior
    {
        private static Dictionary<string, object> CachingBehaviorDictionary = new Dictionary<string, object>();

        public bool WillExecute
        {
            get { return true; }
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            Console.WriteLine("CachingBehavior");
            string key = $"{input.MethodBase.Name}_{Newtonsoft.Json.JsonConvert.SerializeObject(input.Inputs)}";
            if (CachingBehaviorDictionary.ContainsKey(key))
            {
                return input.CreateMethodReturn(CachingBehaviorDictionary[key]);    //Circuit breaker, return directly.
            }
            else
            {
                IMethodReturn result = getNext().Invoke(input, getNext);
                if (result.ReturnValue != null)
                    CachingBehaviorDictionary.Add(key, result.ReturnValue);
                return result;
            }
        }
    }
CachingBehavior.cs (Cache Processing Class)
    /// <summary>
    /// No feature required
    /// </summary>
    public class PermissionBehavior : IInterceptionBehavior
    {
        public bool WillExecute
        {
            get { return true; }
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            Console.WriteLine("PermissionBehavior");
            Console.WriteLine(input.MethodBase.Name);
            foreach (var item in input.Inputs)
            {
                Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(item));
                //reflex&Serialization for more information
            }
            return getNext().Invoke(input, getNext);
        }
    }
PermissionBehavior.cs (Permission Processing Class)
    /// <summary>
    /// No feature required
    /// </summary>
    public class ParameterCheckBehavior : IInterceptionBehavior
    {
        public bool WillExecute
        {
            get { return true; }
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            User user = input.Inputs[0] as User;    //Can not write dead type, reflection+Features complete data validity monitoring.

            Console.WriteLine("ParameterCheckBehavior");
            if (user.Password.Length < 10)          //You can filter out sensitive words
            {
                return input.CreateExceptionMethodReturn(new Exception("Password length cannot be less than 10 bits"));
            }
            else
            {
                return getNext().Invoke(input, getNext);
            }
        }
    }
ParameterCheckBehavior.cs (parameter detection class)
    /// <summary>
    /// No feature required
    /// </summary>
    public class LogBehavior : IInterceptionBehavior
    {
        public bool WillExecute
        {
            get { return true; }
        }

        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }

        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            IMethodReturn methodReturn = getNext()(input, getNext); //Perform all subsequent actions

            Console.WriteLine("LogBehavior");
            Console.WriteLine(input.MethodBase.Name);
            foreach (var item in input.Inputs)
            {
                Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(item));
                //reflex&Serialization for more information
            }
            return methodReturn;
        }
    }
LogBehavior.cs (Log Processing Class)

3) Create a new configuration file, Unity.Config (the code for this example is under the UnityConfigAOP folder), and choose Always Copy when copying its properties to the output directory.

<configuration>
  <configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/>
  </configSections>
  <unity>
    <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration"/>
    <containers>
      <container name="AOPContainer">
        <extension type="Interception"/>
        <!--Registration Matching Rule: Complete type name before, followed by dll Name.-->
        <register type="LinkTo.Test.ConsoleAop.UnityConfigAOP.IUserProcessor,LinkTo.Test.ConsoleAop" mapTo="LinkTo.Test.ConsoleAop.UnityConfigAOP.UserProcessor,LinkTo.Test.ConsoleAop">
          <interceptor type="InterfaceInterceptor"/>
          <!--The interception sequence is top-down; the configuration will execute completely unless a circuit breaker, etc. is encountered; it is recommended that exception handling packages be on the outermost layer, that is, on the top.-->
          <interceptionBehavior type="LinkTo.Test.ConsoleAop.UnityConfigAOP.ExceptionBehavior, LinkTo.Test.ConsoleAop"/>
          <interceptionBehavior type="LinkTo.Test.ConsoleAop.UnityConfigAOP.CachingBehavior, LinkTo.Test.ConsoleAop"/>
          <interceptionBehavior type="LinkTo.Test.ConsoleAop.UnityConfigAOP.PermissionBehavior, LinkTo.Test.ConsoleAop"/>
          <interceptionBehavior type="LinkTo.Test.ConsoleAop.UnityConfigAOP.ParameterCheckBehavior, LinkTo.Test.ConsoleAop"/>
          <interceptionBehavior type="LinkTo.Test.ConsoleAop.UnityConfigAOP.LogBehavior, LinkTo.Test.ConsoleAop"/>
        </register>
      </container>
    </containers>
  </unity>
</configuration>
Unity.Config

4) Call:

        static void Main(string[] args)
        {
            #region Use EntLib\PIAB Unity Implement dynamic proxy(With Configuration)
            UnityConfigAOP.UnityConfigAOP.Show();
            Console.Read();
            #endregion
        }

5) The results are as follows:

3.3. IL Braiding Method

IL braiding can be done using the PostSharp framework, but since Postsharp starts charging from version 2.0, it is not explained here anymore. If you are interested, you can Baidu.(

 

Reference from:

    https://www.cnblogs.com/landeanfen/p/4782370.html

    https://www.cnblogs.com/artech/archive/2011/12/01/autointerception.html

God E Open Class Code

Topics: C# Unity JSON Programming Attribute