iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
2
Software Development

從Asp.net框架角度進入Asp.net MVC原始碼系列 第 20

[Day20] 探討Model上客製化標籤如何被解析使用

前言

上一篇有介紹ModelMetadata和參數Model之間的關係.

UML_Model

MVC提供我們一個IMetadataAware介面,讓我們可以對最終生成ModelMetadata進行自由設定.

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

IMetadataAware介面

IMetadataAware介面有一個OnMetadataCreated方法

public interface IMetadataAware
{
    void OnMetadataCreated(ModelMetadata metadata);
}

MVC有預設兩個實現IMetadataAware介面的標籤.

  • AllowHtmlAttribute:標上的屬性可以攜帶Html資料.
  • AdditionalMetadataAttribute:對於當前屬性的modelmetadata資訊的AdditionalValues添加資料(添加資料可透過ViewData.ModelMetadata.AdditionalValues取得資料)

如果你想要對於modelmetadata資訊做修改或新增資料可以製作自己IMetadataAware介面標籤.

AllowHtmlAttribute標籤

為了防止(Cross-site scripting)XSS攻擊通過在針對某些輸入框中寫入或注入HTML來攻擊我們Web應用

針對HTML標記驗證通過ModelMetadataRequestValidationEnabled來控制,如下面程式碼顯示

這是一個布爾類型的可讀寫屬性。

public class ModelMetadata
{
	public virtual bool RequestValidationEnabled { get; set; } 
}

此屬性在默認情況下為True進行驗證防護

ASP.NET MVC有一個預設標籤AllowHtmlAttribute在進行Model綁定之前會對對應請求資料進行驗證,確保沒有任何HTML標記包含其中。

如果在Input tag輸入有關Html資料就會出現下面錯誤.(這是MVC貼心幫我們開啟防護XSS攻擊的機制)

具有潛在危險Request.Form 的值已從用戶端 (xxxxxx) 偵測到。

UML_Model

如果查看AllowHtmlAttribute原始碼就很簡單只是把metadata.RequestValidationEnabled設成false允許使用者上傳Html資料.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class AllowHtmlAttribute : Attribute, IMetadataAware
{
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        if (metadata == null)
        {
            throw new ArgumentNullException("metadata");
        }

        metadata.RequestValidationEnabled = false;
    }
}

我們就可以把Html資料傳送到AP端了

只是這個標籤請斟酌使用打開有一定風險.

為何可以透過實現IMetadataAware介面來擴充對於metadata操作

AssociatedMetadataProvider抽象類別中有個ApplyMetadataAwareAttributes方法.

參數物件上屬性進行反射取得使用到IMetadataAware的標籤,並呼叫他的OnMetadataCreated方法.

public abstract class AssociatedMetadataProvider : ModelMetadataProvider
{
    private static void ApplyMetadataAwareAttributes(IEnumerable<Attribute> attributes, ModelMetadata result)
    {
        foreach (IMetadataAware awareAttribute in attributes.OfType<IMetadataAware>())
        {
            awareAttribute.OnMetadataCreated(result);
        }
    }
}

CachedDataAnnotationsModelMetadataProvider

MVC Action傳入參數上可以標示許多標籤例如

public class VerifyCodeViewModel
{
	[Required]
	public string Provider { get; set; }

	[Required]
	[Display(Name = "代碼")]
	public string Code { get; set; }
	public string ReturnUrl { get; set; }

	[Display(Name = "記住此瀏覽器?")]
	public bool RememberBrowser { get; set; }

	public bool RememberMe { get; set; }
}

public class ForgotViewModel
{
	[Required]
	[Display(Name = "電子郵件")]
	public string Email { get; set; }
}
  • RequiredAttribute
  • DisplayAttribute

還有其他一大堆,下面會跟大家介紹MVC是怎麼取得並使用這些標籤,ModelMetadataProviders這個類別會提供使用哪個ModelMetadataProvider

在原始碼建構子預設使用CachedDataAnnotationsModelMetadataProvider

public class ModelMetadataProviders
{
    private static ModelMetadataProviders _instance = new ModelMetadataProviders();
    private ModelMetadataProvider _currentProvider;
    private IResolver<ModelMetadataProvider> _resolver;

    internal ModelMetadataProviders(IResolver<ModelMetadataProvider> resolver = null)
    {
        _resolver = resolver ?? new SingleServiceResolver<ModelMetadataProvider>(
                                    () => _currentProvider,
                                    new CachedDataAnnotationsModelMetadataProvider(),
                                    "ModelMetadataProviders.Current");
    }
    //....
}

CachedDataAnnotationsModelMetadataProvider類別有一個CreateMetadataPrototype方法返回一個CachedDataAnnotationsModelMetadata物件,這個物件存放參數上屬性欄位使用標籤資訊.

public class CachedDataAnnotationsModelMetadataProvider : CachedAssociatedMetadataProvider<CachedDataAnnotationsModelMetadata>
{
	protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<Attribute> attributes, Type containerType, Type modelType, string propertyName)
	{
		return new CachedDataAnnotationsModelMetadata(this, containerType, modelType, propertyName, attributes);
	}

	protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
	{
		return new CachedDataAnnotationsModelMetadata(prototype, modelAccessor);
	}
}

CachedDataAnnotationsModelMetadata

CachedDataAnnotationsModelMetadata類別上有許多屬性,主要是方便日後來判斷使用MVC使用標籤

public class CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>
{
    private bool _isEditFormatStringFromCache;

    public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
        : base(prototype, modelAccessor)
    {
    }

    public CachedDataAnnotationsModelMetadata(CachedDataAnnotationsModelMetadataProvider provider, Type containerType, Type modelType, string propertyName, IEnumerable<Attribute> attributes)
        : base(provider, containerType, modelType, propertyName, new CachedDataAnnotationsMetadataAttributes(attributes.ToArray()))
    {
    }

    protected override bool ComputeConvertEmptyStringToNull()
    {
        return PrototypeCache.DisplayFormat != null
                    ? PrototypeCache.DisplayFormat.ConvertEmptyStringToNull
                    : base.ComputeConvertEmptyStringToNull();
    }

    protected override string ComputeDataTypeName()
    {
        if (PrototypeCache.DataType != null)
        {
            return PrototypeCache.DataType.ToDataTypeName();
        }

        if (PrototypeCache.DisplayFormat != null && !PrototypeCache.DisplayFormat.HtmlEncode)
        {
            return DataTypeUtil.HtmlTypeName;
        }

        return base.ComputeDataTypeName();
    }
    //...
}

有一個蠻特別事情是CachedDataAnnotationsModelMetadata : CachedModelMetadata<CachedDataAnnotationsMetadataAttributes>他繼承一個泛型類別CachedDataAnnotationsMetadataAttributes存放取得物件標籤的資訊.

CachedDataAnnotationsMetadataAttributes

CachedDataAnnotationsMetadataAttributes類別主要把屬性上的某些標籤給值到類別的屬性上,方便CachedDataAnnotationsModelMetadata來操作使用.

這也是為什麼只有某些標籤掛在屬性上可以被使用.預設只有CachedDataAnnotationsMetadataAttributes才會被反射取得.

public class CachedDataAnnotationsMetadataAttributes
{
	public CachedDataAnnotationsMetadataAttributes(Attribute[] attributes)
	{
		DataType = attributes.OfType<DataTypeAttribute>().FirstOrDefault();
		Display = attributes.OfType<DisplayAttribute>().FirstOrDefault();
		DisplayColumn = attributes.OfType<DisplayColumnAttribute>().FirstOrDefault();
		DisplayFormat = attributes.OfType<DisplayFormatAttribute>().FirstOrDefault();
		DisplayName = attributes.OfType<DisplayNameAttribute>().FirstOrDefault();
		Editable = attributes.OfType<EditableAttribute>().FirstOrDefault();
		HiddenInput = attributes.OfType<HiddenInputAttribute>().FirstOrDefault();
		ReadOnly = attributes.OfType<ReadOnlyAttribute>().FirstOrDefault();
		Required = attributes.OfType<RequiredAttribute>().FirstOrDefault();
		ScaffoldColumn = attributes.OfType<ScaffoldColumnAttribute>().FirstOrDefault();
        //.....
	}

	public DataTypeAttribute DataType { get; protected set; }

	public DisplayAttribute Display { get; protected set; }

	public DisplayColumnAttribute DisplayColumn { get; protected set; }

	public DisplayFormatAttribute DisplayFormat { get; protected set; }

	public DisplayNameAttribute DisplayName { get; protected set; }

	public EditableAttribute Editable { get; protected set; }

	public HiddenInputAttribute HiddenInput { get; protected set; }
    //.....
}

小結:

ModelMetaData是一個Model Binding很重要物件,裡面存放許多調用參數的資訊.

MVC提供一個IMetadataAware介面可以改變ModelMetaData中資訊,提高更高的彈性.

這篇也介紹了IMetadataAware介面是在哪邊做攔截.

另外也分享常掛在屬性上標籤取得的類別跟機制

  • DisplayNameAttribute
  • RequiredAttribute
  • DisplayAttribute

透過CachedDataAnnotationsModelMetadataProvider這個類別來取得以上標籤,並在日後做判斷.

下篇會和大家分享另一種屬性標籤ValidationAttribute的取得和呼叫過程.


上一篇
[Day19] Http參數如何綁定到Action參數上(簡單和複雜模型綁定探討)
下一篇
[Day21] Model 探討驗證標籤(ValidationAttribute)
系列文
從Asp.net框架角度進入Asp.net MVC原始碼30

尚未有邦友留言

立即登入留言