Agenda
前言 今天要分享對於ActionInvoker
進行替換成自己客制化的IActionInvoker
在MVC 原始碼中有個CreateActionInvoker
方法來取得一個IActionInvoker
物件,可以看到她會先透過Resolver.GetService
從解析器中取得我們的IActionInvoker
如果沒有在new
一個AsyncControllerActionInvoker
物件.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected virtual IActionInvoker CreateActionInvoker (){ IAsyncActionInvokerFactory asyncActionInvokerFactory = Resolver.GetService<IAsyncActionInvokerFactory>(); if (asyncActionInvokerFactory != null ) { return asyncActionInvokerFactory.CreateInstance(); } IActionInvokerFactory actionInvokerFactory = Resolver.GetService<IActionInvokerFactory>(); if (actionInvokerFactory != null ) { return actionInvokerFactory.CreateInstance(); } return Resolver.GetService<IAsyncActionInvoker>() ?? Resolver.GetService<IActionInvoker>() ?? new AsyncControllerActionInvoker(); }
我們解析器一樣使用Autofac
容器來幫我們完成(程式碼會基於昨天Autofac
範例往上擴充)
建立自己的IActionInvoker(CustomerActionInvoker) 在取得IActionInvoker
首先會透過Resolver
解析器來取得,這就提供我們一個可替換接口.
藉由這個機制讓我們可以重寫自己ActionInvoker
物件.
我們自行撰寫的CustomerActionInvoker
支援簡單模型綁定(這個版本支援由Request.Form
和Request.QueryString
參數綁定)
首先利用反射先取得呼叫Action
方法資訊,我再呼叫BindModel
利用linq
對於Action
方法需要參數進行動態綁定
BindModel
方法中先判斷目前參數型別是否是字串型別,如果是透過GetValueTypeInstance
從ValueProvider
(Request.Form
和Request.QueryString
)取值,如果方法使用參數非簡單型別參數就會呼叫SimpleModelBinding
方法
SimpleModelBinding
利用反射動態建立此物件,取得此物件屬性資訊並一一把值給填充到屬性上.
在SimpleModelBinding
會判斷屬性型別和可否寫入!property.CanWrite || IsSimpleType(property)
來填值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public class CustomerActionInvoker : IActionInvoker { public bool InvokeAction (ControllerContext controllerContext, string actionName ) { MethodInfo method = controllerContext.Controller .GetType() .GetMethods() .First(m => string .Compare(actionName, m.Name, StringComparison.OrdinalIgnoreCase) == 0 ); var parameters = method.GetParameters().Select(parameter => BindModel(controllerContext, parameter.Name, parameter.ParameterType)); ActionResult actionResult = method.Invoke(controllerContext.Controller, parameters.ToArray()) as ActionResult; actionResult.ExecuteResult(controllerContext); return true ; } private object BindModel (ControllerContext controllerContext,string modelName, Type modelType ) { if (modelType.IsValueType || typeof (string ) == modelType) { object instance; if (GetValueTypeInstance(controllerContext, modelName, modelType, out instance)) { return instance; } return Activator.CreateInstance(modelType); } return SimpleModelBinding(controllerContext, modelType); } private object SimpleModelBinding (ControllerContext controllerContext, Type modelType ) { object modelInstance = Activator.CreateInstance(modelType); foreach (PropertyInfo property in modelType.GetProperties()) { if (!property.CanWrite || IsSimpleType(property)) { object propertyValue; if (GetValueTypeInstance(controllerContext, property.Name, property.PropertyType, out propertyValue)) { property.SetValue(modelInstance, propertyValue); } } } return modelInstance; } private bool GetValueTypeInstance (ControllerContext controllerContext, string modelName, Type modelType, out object value ) { var form = controllerContext.RequestContext.HttpContext.Request.Form; var queryString = controllerContext.RequestContext.HttpContext.Request.QueryString; string key = form.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 ); if (key != null ) { value = Convert.ChangeType(form[key], modelType); return true ; } string queryKey = queryString.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 ); if (queryKey != null ) { value = Convert.ChangeType(queryString[queryKey], modelType); return true ; } value = null ; return false ; } private static bool IsSimpleType (PropertyInfo property ) { return property.PropertyType == typeof (string ) || property.PropertyType.IsValueType; } }
最後在Autofac
中多註冊一組IActionInvoker
,MVC 就會使用CustomerActionInvoker
而不是原本的ControllerActionInvoker
1 builder.RegisterType<CustomerActionInvoker>().As<IActionInvoker>();
進行呼叫測試 我在HomeController
下新增一個About
方法傳入一個Person
類別.
後面請求 http:xxx/Home/About?name=daniel
我們就可以看到方法使用p
參數已經可以成功填值瞜
1 2 3 4 5 6 7 8 9 10 11 public class Person { public string Name{ get ; set ; } } public ActionResult About (Person p ){ ViewBag.Message = $"Member {p?.Name??string .Empty} Balance { _service.GetMemberBalance(123 )} " ; return View(); }
改進GetValueTypeInstance方法(建立ValueProvider) 在GetValueTypeInstance
方法中透過Http
上請求獲取資料目前有兩種方式Request.Form
和Request.QueryString
,我們可以看到上面的方法有許多重複程式碼
這次要做動作是重構 把上面重複程式碼提取到一個父類別(長出父類別或介面).
我覺得在物件導向程式設計介面和父類別是長出來,寫一寫code發現有重複的部分就可以考慮提取方法或提取成父類別.
首先我們先對於GetValueTypeInstance
進行分析.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private bool GetValueTypeInstance (ControllerContext controllerContext, string modelName, Type modelType, out object value ){ var form = controllerContext.RequestContext.HttpContext.Request.Form; var queryString = controllerContext.RequestContext.HttpContext.Request.QueryString; string key = form.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 ); if (key != null ) { value = Convert.ChangeType(form[key], modelType); return true ; } string queryKey = queryString.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 ); if (queryKey != null ) { value = Convert.ChangeType(queryString[queryKey], modelType); return true ; } value = null ; return false ; }
發現到下面這段程式碼基本是重複的除了一個是透過form
,另一個是透過queryString
取得比對取得使用key
.
1 2 3 4 5 6 string key = form.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 );if (key != null ){ value = Convert.ChangeType(form[key], modelType); return true ; }
看到重複動作就可以考慮提取成抽象並把特徵交給子類別來實現或提供.
建立一個ValueProviderBase抽象類別 在下面有一個GetValue
方法我們把上面重複的程式碼放進裡面,提供一個abstract NameValueCollection nameValueCollection
抽象屬性給自類別提供實現.
因為QueryString
和Form
都是NameValueCollection
型態的集合.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public abstract class ValueProviderBase { protected ControllerContext _controllerContext; public ValueProviderBase (ControllerContext controllerContext ) { _controllerContext = controllerContext; } protected abstract NameValueCollection nameValueCollection { get ; } public object GetValue (string modelName,Type modelType ) { string key = nameValueCollection.AllKeys.FirstOrDefault(x => string .Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0 ); if (key != null ) { return Convert.ChangeType(nameValueCollection[key], modelType); } return null ; } }
建立兩個類別FormValueProvider
,QueryStringValueProvider
繼承於ValueProviderBase
並實現NameValueCollection
抽象屬性
FormValueProvider
:提供Request.Form
QueryStringValueProvider
:提供Request.QueryString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class FormValueProvider : ValueProviderBase { public FormValueProvider (ControllerContext controllerContext ) : base (controllerContext ) { } protected override NameValueCollection nameValueCollection => _controllerContext.RequestContext.HttpContext.Request.Form; } public class QueryStringValueProvider : ValueProviderBase { public QueryStringValueProvider (ControllerContext controllerContext ) : base (controllerContext ) { } protected override NameValueCollection nameValueCollection => _controllerContext.RequestContext.HttpContext.Request.QueryString; }
最後在GetValueTypeInstance
方法會改寫成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private bool GetValueTypeInstance (ControllerContext controllerContext, string modelName, Type modelType, out object value ){ List<ValueProviderBase> _valueProvider = new List<ValueProviderBase>() { new FormValueProvider(controllerContext), new QueryStringValueProvider(controllerContext) }; foreach (var valueProvider in _valueProvider) { value = valueProvider.GetValue(modelName, modelType); if (value != null ) return true ; } value = null ; return false ; }
建立一個列表存放ValueProvider
集合並使用迴圈來一個一個判斷是否有值匹配到.
改寫完後有沒有發覺GetValueTypeInstance
方法比上面版本更好理解呢?
我把細部邏輯都封裝到類別中,閱讀上也變得更容易.
似成相識IValueProvider介面 還記得之前我們有介紹到一個IValueProvider
介面提供一個重要方法GetValue
如何從Http
請求中取得資料藉由傳入key
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public interface IValueProvider { bool ContainsPrefix (string prefix ) ; ValueProviderResult GetValue (string key ) ; }
這次重構IValueProvider
很類似之前介紹的IValueProvider
介面,上面List<ValueProviderBase>
就是之前介紹ValueProviderFactories
工廠.
小結: 今天利用一個範例建立自己的簡單模型綁定ActionInvoker 向大家分享如何建立自己的ActionInvoker
只需要透過一個Resolver
解析器和繼承IActionInvoker
即可完成.
後面再利用重構技巧優化本次程式.希望今天使用到的技巧對於大家有所幫助
設計模式不是把程式碼變簡單而是整理得更有條理(程式碼可能會更複雜但卻很合理,更好去理解複雜邏輯)
一個房間很亂經過整理後東西不會變少(排除丟掉東西),但物品位置會變得更有條理
Github範例程式原始碼 customerActionInvoker
分支上
__此文作者__:Daniel Shih(石頭) __此文地址__: https://isdaniel.github.io/Ithelp-day28/ __版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!