2015年12月16日 星期三

【WebAPI】Custom ActionFilter Order


環境 : WebAPI 2.2 (詳細)

在WebAPI中,通常都繪有需要客製化ActionFilter的情況發生


但這些ActionFilter的執行順序卻是不固定的,並非排在越上面的越先執行,Filter有個FilterScope來表示他屬於何種層級的ActionFilter


執行順序依次是 Global , Controller , Action 。但如果是在同一層級的ActionFilter順序則未必按照固定。 可以由以下方式做個簡單的測試

先新增四個ActionFilter,




裡面全部都一樣,都繼承ActionFilterAttribute即可

public class FourAttribute :ActionFilterAttribute{ }



接著把OneAttribute註冊到Global



將剩下的分別註冊到Controller與Action上



在Get中補上以下程式然後執行看看

        [HttpGet]
        [Three]
        [Four]
        public IEnumerable<Tuple<string,string>> Get()
        {
            IHttpActionSelector actionSelector =
                this.Configuration.Services.GetActionSelector();

            HttpActionDescriptor actionDescriptor =
                actionSelector.SelectAction(this.ControllerContext);

            foreach (FilterInfo filterInfo in actionDescriptor.GetFilterPipeline())
            {
                yield return new Tuple<string, string>(
                    filterInfo.Instance.GetType().Name,
                    filterInfo.Scope.ToString()
                );
            }
        }


執行結果如下


順序的排序依據,只依照到所對應的層級,雖然這邊排序與我們在程式上的排序相同。但我這邊也實際碰到過順序變動過的狀況,為了避免這樣的問題,最好的方法就是在ActionFilter加上排序的屬性來解決。



  • 首先先做一個InterFace讓所有Attribute都實做它
    public interface IAttribute
        {
            int Order { get; set; }
        }
    

    public class FourAttribute : ActionFilterAttribute, IAttribute
        {
            public int Order { get; set; }
        }
    
  • CustomFilterInfo實做IComparable介面
    public class CustomFilterInfo : IComparable
        {
            public IFilter Instance { get; set; }
            public FilterScope Scope { get; set; }
    
            //FilterInfo
            public CustomFilterInfo(IFilter instance, FilterScope scope)
            {
                this.Instance = instance;
                this.Scope = scope;
            }
    
            public int CompareTo(object obj)
            {
                if (obj is CustomFilterInfo)
                {
                    var item = obj as CustomFilterInfo;
    
                    if (item.Instance is IAttribute)
                    {
                        var attr = item.Instance as IAttribute;
                        return (this.Instance as IAttribute).Order.CompareTo(attr.Order);
                    }
                }
    
                return 0;
            }
    
            public FilterInfo ConvertToFilterInfo()
            {
                return new FilterInfo(this.Instance, this.Scope);
            }
        }
    
  • CustomFilterProvider實做IFilterProvider介面
    public class CustomFilterProvider : IFilterProvider
        {
            public IEnumerable<FilterInfo> GetFilters(
                HttpConfiguration configuration, 
                HttpActionDescriptor actionDescriptor)
            {
                IEnumerable<CustomFilterInfo> customActionFilters =
                    actionDescriptor.GetFilters()
                                    .Select(i => new CustomFilterInfo(i, FilterScope.Controller));
    
                IEnumerable<CustomFilterInfo> customControllerFilters =
                    actionDescriptor.ControllerDescriptor
                                    .GetFilters()
                                    .Select(i => new CustomFilterInfo(i, FilterScope.Controller));
    
                return customControllerFilters.Concat(customActionFilters)
                                              .OrderBy(i => i)
                                              .Select(i => i.ConvertToFilterInfo());
    
            }
        }
    
  • 接著在Global將預設的FilterProvider移除,加上客製化的CustomFilterProvider
    protected void Application_Start()
            {
                AreaRegistration.RegisterAllAreas();
                GlobalConfiguration.Configure(WebApiConfig.Register);
                FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
                GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
    
                //註冊Global Attribute
                GlobalConfiguration.Configuration.Filters.Add(new OneAttribute());
    
                
                //新增CustomFilterProvider
                GlobalConfiguration.Configuration.Services.Add(
                    typeof(System.Web.Http.Filters.IFilterProvider), new CustomFilterProvider());
    
                var providers = GlobalConfiguration.Configuration.Services.GetFilterProviders();
                var defaultprovider = providers.First(i => i is ActionDescriptorFilterProvider);
    
                //移除DefaultProvider
                GlobalConfiguration.Configuration.Services.Remove(
                    typeof(System.Web.Http.Filters.IFilterProvider),
                    defaultprovider);
            }
    
  • 回到剛剛的Action將Attibute補上Order再執行看看得到的順序
  • 結果可以看到Four排在Three的前面了,完成!!


2015年12月13日 星期日

【SQL】遞迴


有張表格如下,Parent欄位代表他的父層的RegionID,所以裡面包含所有縣市、行政區、鄉里等資料,那要如何用SQL遞迴的方式把台北市用階層的方式表列出來呢?
台北市 > 信義區、大安區、中正區.... > 港華里、老泉里.....



用CTE的寫法可以解決,把台北市當做茅點,然後Join自己即可達到遞迴的效果

with 遞迴 as(
------茅點 start-------
SELECT RegionID , Name
FROM Region a
WHERE RegionID = 1 --台北市
------茅點 end ---------
union all
select b.RegionID , b.name
from Region b
join 遞迴 on  b.Parent = 遞迴.RegionID
)
select * from 遞迴


搜尋出來的結果

2015年12月8日 星期二

AutoMapper與Json.NET JObject對應問題


使用套件:
Json.NET 7.0.1 : https://www.nuget.org/packages/Newtonsoft.Json/7.0.1
AutoMapper 4.04 : https://www.nuget.org/packages/AutoMapper/4.0.4

今天碰到一個問題,就是有個API回傳值的欄位是不固定無法掌握的,所以只好在轉型成強型別時以object當做該屬性的類別,但JsonConvert碰到類別為Object的東西就會轉成JObject ,而AutoMapper對應JObject會炸掉。以下是簡單時間的範例Code

void Main()
{
 Mapper.CreateMap<source, destination>()
 .ForMember(d => d.d_name, o => o.MapFrom(s => s.name))
 .ForMember(d => d.d_obj, o => o.MapFrom(s => s.obj));

 source test = new source 
 {
  name = "test",
  obj = new {code = 100},
 };

 var result = Mapper.Map<destination>(test);
 result.Dump();
}

public class source
{
 public string name { get; set; }
 public object obj { get; set; }
}

public class destination
{
 public string d_name { get; set; }
 public object d_obj { get; set; }
}

Source與Destination都有個property為object的類別屬性,在Main()裡面也想好兩個類別的對應關係,並且先準備好Source 然後透過AutoMapper轉出Result,在以上的範例執行正確沒問題




換個寫法

void Main()
{
 Mapper.CreateMap<source, destination>()
 .ForMember(d => d.d_name, o => o.MapFrom(s => s.name))
 .ForMember(d => d.d_obj, o => o.MapFrom(s => s.obj));

 source test = JsonConvert.DeserializeObject<source>("{\"name\":\"test\",\"obj\" : {\"code\":100}}");
 var result = Mapper.Map<destination>(test);
 result.Dump();
}

public class source
{
 public string name { get; set; }
 public object obj { get; set; }
}

public class destination
{
 public string d_name { get; set; }
 public object d_obj { get; set; }
}

差別只在於原本Source改成透過Json.Net由字串轉回來而已,這時候只要執行到AutoMapper那一行就會爆炸,錯誤訊息如下
AutoMapperMappingException: 

Mapping types:
JObject -> JObject
Newtonsoft.Json.Linq.JObject -> Newtonsoft.Json.Linq.JObject

Destination path:
destination.d_obj.d_obj

Source value:
{
  "code": 100
}


JsonConvert碰到目標為Object型別的欄位,會轉成JObject塞進去,AutoMapper用它來對應,
所以如果要解決這個問題需要做一些處理

//將Mapper改成如下
 Mapper.CreateMap<source, destination>()
 .ForMember(d => d.d_name, o => o.MapFrom(s => s.name))
 .ForMember(d => d.d_obj, o => o.ResolveUsing(s =>
 {
  if (s.obj is JObject)
  {
   var temp = s.obj as JObject;
   return temp.ToObject<Dictionary<string, object>>();
  }
  return s.obj;
 }));

這樣就可以正確地取出了

2015年12月3日 星期四

【MVC】EditorTemplate (二) 動態新增欄位


此篇範例程式 : 下載  (此範例在DynamicController之中)


呈上一篇,EditorTemplate (一) 我們可以來看到產生的原始碼如下







Name的部分是由 productList[index].屬性名組成,換句話說,如果今天要由前端動態新增一筆書籍資料,則必須按照這個規則編排下去,後端才能透過ViewModel的方式取得書籍資料










看起來一切美好圓滿,但如果今天的列表是可以新增之外,還要能動態刪除呢?
是不是我的[index]就要一直重新計算,不然送到後端就會不見了
*如果今天有4個textbox但是name分別是
productList[0].id
productList[1].id
productList[2].id
productList[5].id
這樣後端只會拿到0~2的ID,不按照順序編排的就會消失

還好.NET其實提供另外一種方式來繫結ViewModel
<input type="hidden" name="productList.Index" value="0072b890-0e1a-4c93-a5a7-9cafe84b65f8" /> 
<input  id="productList_0072b890-0e1a-4c93-a5a7-9cafe84b65f8__id" name="productList[0072b890-0e1a-4c93-a5a7-9cafe84b65f8].id" type="text" > 


這樣的話就可以不用管排序,自由的新增刪除List的項目,但EditorTemplate也需要改一改,不然EditTemplate產出的是[0]這種格是,但是前端產出的格是是[Guid],這樣會有問題。

所以EditTemplate改成用這個方法產出
*以下程式碼轉載自:  
搞搞就懂 - 點部落 :[ASP Net MVC] 如何綁定可動態新增或移除之資料集合(EditorTemplate)

  • 新增一組HtmlHelper
     public static MvcHtmlString EditorForMany<TModel, TValue>(
                this HtmlHelper<TModel> html,
                Expression<Func<TModel, IEnumerable<TValue>>> expression,
                string htmlFieldName = null) where TModel : class
            {
                var items = expression.Compile()(html.ViewData.Model);
                var sb = new StringBuilder();
                var hasPrefix = false;
    
                if (String.IsNullOrEmpty(htmlFieldName))
                {
                    var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;
                    hasPrefix = !String.IsNullOrEmpty(prefix);
                    htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);
                }
    
                if (items != null)
                {
                    foreach (var item in items)
                    {
                        var dummy = new { Item = item };
                        var guid = Guid.NewGuid().ToString();
    
                        var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
                        var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);
    
                        sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));
                        sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", hasPrefix ? ExpressionHelper.GetExpressionText(expression) : htmlFieldName, guid)));
                    }
                }
                return new MvcHtmlString(sb.ToString());
            }
        }
    
  • 原本EditorFor改成自訂的EditorForMany
    @Html.EditorFor(x => x.productList)
    @Html.EditorForMany(x => x.productList)
    



這樣產出來的格式就會是帶Guid的方式了,可以正常跟前端結合了