今天要分享對於ActionInvoker
進行替換成自己客制化的IActionInvoker
在MVC原始碼中有個CreateActionInvoker
方法來取得一個IActionInvoker
物件,可以看到她會先透過Resolver.GetService
從解析器中取得我們的IActionInvoker
如果沒有在new
一個AsyncControllerActionInvoker
物件.
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();
}
// Note that getting a service from the current cache will return the same instance for every request.
return Resolver.GetService<IAsyncActionInvoker>() ??
Resolver.GetService<IActionInvoker>() ??
new AsyncControllerActionInvoker();
}
我們解析器一樣使用Autofac
容器來幫我們完成(程式碼會基於昨天Autofac
範例往上擴充)
在取得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)
來填值.
public class CustomerActionInvoker : IActionInvoker
{
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
//取得執行Action方法
MethodInfo method = controllerContext.Controller
.GetType()
.GetMethods()
.First(m => string.Compare(actionName, m.Name, StringComparison.OrdinalIgnoreCase) == 0);
//取得Action使用的參數,並利用反射將值填充
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())
{
//針對基本型別或string型別給值
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
builder.RegisterType<CustomerActionInvoker>().As<IActionInvoker>();
我在HomeController
下新增一個About
方法傳入一個Person
類別.
後面請求 http:xxx/Home/About?name=daniel
我們就可以看到方法使用p
參數已經可以成功填值瞜
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
方法中透過Http
上請求獲取資料目前有兩種方式Request.Form
和Request.QueryString
,我們可以看到上面的方法有許多重複程式碼
這次要做動作是重構把上面重複程式碼提取到一個父類別(長出父類別或介面).
我覺得在物件導向程式設計介面和父類別是長出來,寫一寫code發現有重複的部分就可以考慮提取方法或提取成父類別.
首先我們先對於GetValueTypeInstance
進行分析.
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
.
string key = form.AllKeys.FirstOrDefault(x => string.Compare(x, modelName, StringComparison.OrdinalIgnoreCase) == 0);
if (key != null)
{
value = Convert.ChangeType(form[key], modelType);
return true;
}
看到重複動作就可以考慮提取成抽象並把特徵交給子類別來實現或提供.
在下面有一個GetValue
方法我們把上面重複的程式碼放進裡面,提供一個abstract NameValueCollection nameValueCollection
抽象屬性給自類別提供實現.
因為
QueryString
和Form
都是NameValueCollection
型態的集合.
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
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
方法會改寫成
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
介面提供一個重要方法GetValue
如何從Http
請求中取得資料藉由傳入key
.
/// <summary>
/// Defines the methods that are required for a value provider in ASP.NET MVC.
/// </summary>
public interface IValueProvider
{
/// <summary>
/// Determines whether the collection contains the specified prefix.
/// </summary>
bool ContainsPrefix(string prefix);
/// <summary>
/// Retrieves a value object using the specified key.
/// </summary>
ValueProviderResult GetValue(string key);
}
這次重構IValueProvider
很類似之前介紹的IValueProvider
介面,上面List<ValueProviderBase>
就是之前介紹ValueProviderFactories
工廠.
今天利用一個範例建立自己的簡單模型綁定ActionInvoker向大家分享如何建立自己的ActionInvoker
只需要透過一個Resolver
解析器和繼承IActionInvoker
即可完成.
後面再利用重構技巧優化本次程式.希望今天使用到的技巧對於大家有所幫助
設計模式不是把程式碼變簡單而是整理得更有條理(程式碼可能會更複雜但卻很合理,更好去理解複雜邏輯)
一個房間很亂經過整理後東西不會變少(排除丟掉東西),但物品位置會變得更有條理
Github範例程式原始碼 customerActionInvoker
分支上