Chưa phân loại

C# Tutorial – Tạo và sử dụng Custom Attribute

Đây là 1 bài viết khá hay, mình đọc được bên http://yinyangit.wordpress.com/
Do không có thời gian nên mình xin phép lấy nguyên văn nội dung lý thuyết của bài viết, và ở cuối bài, mình sẽ có 1 bài demo nho nhỏ để các bạn hiểu làm thế nào để làm việc với CustomAttribute

Tạo lớp Attribute

Để tạo một attribute mới, bạn phải tạo một lớp kế thừa từ System.Attribute đồng thời sử dụng attribute AttributeUsage để khai báo các thông tin cần thiết.
Khi đặt tên cho attribute sắp tạo ra này, bạn nên tuân theo quy tắc của Microsoft là thêm hậu tố “Attribute” vào tên lớp. Bạn không cần phải lo lắng nếu như tên lớp quá dài, khi sử dụng IDE sẽ tự động bỏ phần “Attribute” phía sau đi, và bạn có thể sử dụng cả hai tên của attribute, ví dụ như ObsoleteObsoleteAttribute là tương đương nhau.
Bây giờ ta thử tạo ra một attribute đơn giản để lưu tên tác giả và địa chỉ web, một ví dụ rất thực tế và cần thiết:

class DemoAttribute : System.Attribute
{
    public string Author { get; set; }
    public string Url { get; set; }

    public DemoAttribute(string author)
    {
        this.Author = author;
    }
    public override string ToString()
    {
        return String.Format("Author: {0}\nLocation: {1}", Author, Url);
    }
}

Positional Parameter và Named Parameter

Bây giờ hãy tạo một lớp mới để test attribute này. Cách sử dụng tương tự như khi với các attribute của .Net.

[Demo("YinYang",Url="http://yinyangit.wordpress.com")]
class DemoClass
{
    public void Print(string message)
    {
        Console.WriteLine(message);
    }
}

Bạn có thể thấy khi gõ đến phần tham số thứ hai của attribute trên, chức năng IntelliSense hiển thị tooltip hướng dẫn như sau:

Trích dẫn:

DemoAttribute.DemoAttribute(string author, Named Parameters…)

Rõ ràng lớp DemoAttribute ta chỉ định nghĩa duy nhất một constructor yêu cầu tham số author, nhưng khi sử dụng ở trên bạn có lại có thể gán thêm giá trị cho tất cả property miễn là chúng có thể truy xuất được để gán dữ liệu (ngoại trừ các static property).
Mặc dù thông tin này không quan trọng nhưng bạn cũng cần biết để phân biệt rõ ràng. Trong một attribute phân biệt 2 loại tham số, một loại là Positional Parameter và loại kia là Named Parameter. Bạn có thể nhận biết chúng qua các tham số sử dụng trong constructor của attribute, tham số nào được sử dụng thì chúng là Positional Parameter và chúng là bắt buộc mỗi khi sử dụng attribute, còn lại là Named Parameter và chúng là tùy chọn.
Vậy trong attribute trên thì Author là Positional Parameter và Url là Named Parameter.
Các attribute parameter chỉ cho phép bạn sử dụng các kiểu dữ liệu đơn giản sau (trích từ MSDN):

• Simple types (bool, byte, char, short, int, long, float, and double)
• string
• System.Type
• enums
• object (The argument to an attribute parameter of type object must be a constant value of one of the above types.)
• One-dimensional arrays of any of the above types

Để gán giá trị cho các Named Parameter, bạn chỉ cần gõ tên của nó theo sau là dấu “=” và cuối cùng là giá trị cần gán, như ví dụ ở trên là cách tôi gán địa chỉ cho tham số Url.

Sử dụng AttributeUsage Attribute

Như đã nói lúc đầu, sau khi tạo lớp bạn phải dùng attribute AttributeUsage để xác định các kiểu thành phần nào có thể sử dụng được attribute mà bạn tạo ra và một số thông tin liên quan khác. Mặc định nếu không dùng AttributeUsage này để chỉ rõ thì giá trị của nó sẽ là All, tức là attribute của bạn có thể được sử dụng bởi mọi loại thành phần như class, struct, method, enum,…
Khai báo của attribute này có dạng sau:

[AttributeUsage(
   ValidOn,
   AllowMultiple=allowmultiple,
   Inherited=inherited
)]

Trong đó:
ValidOn: xác định thành phần nào có thể sử dụng được attribute. Giá trị này có kiểu enum AttributeTargets và có thể kết hợp nhiều giá trị với nhau bằng toán tử bit hoặc “|”. Mặc định là All.

AllowMultiple: Một giá trị bool, chỉ định rằng attribute có cho phép sử dụng nhiều lần trên một thành phần không. Mặc định là false, giá trị true hầu như không bao giờ được sử dụng vì bạn chỉ cần dùng attribute một lần duy nhất là đủ để khai báo các thông tin cần thiết cho thành phần mà nó được gắn vào.

Inherited: Một kiểu bool có giá trị mặc định là true. Tham số này xác định rằng attribute có thể được thừa kế từ một lớp cha mà nó đã được sử dụng không. Để hiểu rõ hơn tôi sẽ cung cấp một ví dụ nhỏ ở cuối bài.

Bây giờ là lúc bạn áp dụng những kiến thức vừa được trình bày, ở đây tôi muốn attribute DemoAttribute chỉ có thể được sử dụng bởi các class, struct và method. Các tham số AllowMultiple và Inherited không cần thay đổi. Vậy tôi gắn AttributeUsage vào lớp DemoAttribute như sau:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method)]
class DemoAttribute : System.Attribute
{
   // […]
}

Truy xuất các attribute trong quá trình Runtime

Đây cũng là một kĩ thuật reflection mà tôi đã trình bày trong một bài trước đây. Bạn có thể dùng một trong hai cách sau để lấy về các Custom Attribute từ một kiểu dữ liệu nào đó (trong ví dụ này là lớp Program):

Attribute[] attributes = Attribute.GetCustomAttributes(typeof(Program));

object[] attributes = typeof(Program).GetCustomAttributes(true);

Cách đầu tiên thường được sử dụng hơn vì nó trả về một mảng Attribute và ta có thể sử dụng những thành phần của attribute mà không cần phải ép kiểu. Ngoài ra, bạn cũng có thể lấy các Custom Attribute từ các method, event, property,… bằng cách gọi phương thức GetCustomAttributes() tương ứng trong các đối tượng thuộc kiểu MemberInfo, MethodInfo, EventInfo,…
Ví dụ sau sẽ in ra thông tin của DemoAttribute mà ta gắn vào lớp Program, thông qua phương thức ToString():

[Demo("YinYang", Url = "http://yinyangit.wordpress.com")]
class Program
{
    static void Main(string[] args)
    {

        Attribute[] attributes = Attribute.GetCustomAttributes(typeof(Program));
        foreach (var attr in attributes)
        {
            Console.WriteLine(attr);
        }

        Console.Read();
    }
}

Bạn hãy chạy thử và xem chương trình sẽ in ra đúng theo những gì mà ta đã viết trong phương thức ToString() của DemoAttribute.

Code:
Author: YinYang
Location: http://yinyangit.wordpress.com
Ví dụ về tham số Inherited của AttributeUsage

Đầu tiên bạn tạo ra hai lớp DemoClass1DemoClass2 thừa kế từ DemoClass1:

[Demo("DemoClass1")]
class DemoClass1
{
    public virtual void Print(string message)
    {
        Console.WriteLine(message);
    }
}

class DemoClass2:DemoClass1
{
    public override void Print(string message)
    {
        base.Print(message);
    }
}

Bạn có thể tự hỏi liệu DemoClass2 có sẵn DemoAttribute kế thừa từ DemoClass1 không. Điều này tùy thuộc vào giá trị của tham số Inherited mà bạn gán khi dùng AttributeUsage cho lớp DemoAttribute. Bây giờ để mặc định Inherited=true, bạn hãy chạy thử phương thức Main() của lớp Program sau:

class Program
{
    static void Main(string[] args)
    {
        Attribute[] attributes = Attribute.GetCustomAttributes(typeof(DemoClass2));
        foreach (var attr in attributes)
        {
            Console.WriteLine(attr);
        }

        Console.Read();
    }
}

Kết quả xuất ra là:

Code:
Author: DemoClass1
Mặc dù bạn lấy các Custom Attribute từ DemoClass2 nhưng kết quả in ra lại cho thấy nó có giá trị của attribute mà bạn gán cho DemoClass1. Nguyên nhân là attribute này đã được kế thừa từ DemoClass1 sang DemoClass2.
Bây giờ bạn hãy sửa tham số Inherited=false và chạy lại chương trình, kết quả là màn hình Console không xuất hiện gì ngoài dấu nhắc lệnh.

Từ ví dụ trên bạn có thể suy ra được công dụng của tham số Inherited này của AttributeUsage. Việc để tham số này có giá trị mặc định sẽ không chính xác trong ví dụ về DemoAttribute này. Khi bạn tạo một lớp kế thừa những thông tin tác giả từ lớp cha, trong khi tác giả lại là một người khác, vậy việc cần làm khi sau khi viết một lớp kế thừa là gắn một DemoAttribute vào lớp con vừa được tạo ra nhằm cập nhật thông tin hợp lý.

Đó là phần mình tham khảo từ trang http://yinyangit.wordpress.com/

Sau đây là 1 bài demo hoàn chỉnh do mình tự viết.

Giả sử chúng ta có 1 Website, có các page như HomePage (mọi người đều có thể vào), InternalPage (Chỉ có thành viên của website có thể vào) và AdminPage (Chỉ có ban quản trị mới có thể vào). Bây giờ mình sẽ set quyền truy cập lên trên từng trang bằng cách tạo ra 1 attribute có tên AccessAttribute và thiết đặt lên từng trang, các bạn xem code để hiểu rõ hơn.

using System;
using System.Linq;
using System.Reflection;

namespace CustomAttribute
{
    /// <summary>
    /// Enum of WebSite methods
    /// Each item represent for a method name
    /// </summary>
    public enum WebSiteMethod
    {
        HomePage,
        InternalPage,
        AdminPage
    }

    /// <summary>
    /// User role enum
    /// </summary>
    internal enum Role
    {
        Admin,
        User,
        Guest
    }

    /// <summary>
    /// Utility class used to validate user permission and get method name by method enum item
    /// </summary>
    internal class Utility
    {
        /// <summary>
        /// Validate access permission on a method of an user
        /// </summary>
        /// <param name="role"></param>
        /// <param name="classType"></param>
        /// <param name="methodName"></param>
        /// <returns></returns>
        public static bool ValidatePermission(Role role, Type classType, string methodName)
        {
            MethodInfo mi = classType.GetMethod(methodName);

            var attrs = mi.GetCustomAttributes(typeof (AccessAttribute), false);

            AccessAttribute req = attrs.Cast<AccessAttribute>().FirstOrDefault();

            return (req == null) || ((req.Role.Contains(role)));
        }

        /// <summary>
        /// Get method name by method enum
        /// </summary>
        /// <param name="method"></param>
        /// <returns></returns>
        public static string GetMethodName(WebSiteMethod method)
        {
            string methodName = string.Empty;
            switch (method)
            {
                case WebSiteMethod.AdminPage:
                    methodName = "AdminPage";
                    break;
                case WebSiteMethod.HomePage:
                    methodName = "HomePage";
                    break;
                case WebSiteMethod.InternalPage:
                    methodName = "InternalPage";
                    break;
            }
            return methodName;
        }
    }

    /// <summary>
    /// User class
    /// </summary>
    internal class User
    {
        public string UserName { get; set; }
        public Role Role { get; set; }

        public User(string newUserName, Role newRole)
        {
            UserName = newUserName;
            Role = newRole;
        }
    }

    /// <summary>
    /// WebSite class. This is a simulator of a real website
    /// We have 3 pages: Home page, Admin page and Internal page
    /// </summary>
    internal class WebSite
    {
        /// <summary>
        /// Any person has permission to access to home page
        /// </summary>
        public static void HomePage()
        {
            Console.WriteLine("Go to http://www.mysite.com/home");
        }

        /// <summary>
        /// Only admin can access to admin page
        /// </summary>
        [Access(true, Role.Admin)]
        public static void AdminPage()
        {
            Console.WriteLine("Go to http://www.mysite.com/control/admin");
        }

        /// <summary>
        /// Only admin and user can access to internal page
        /// </summary>
        [Access(true, Role.Admin, Role.User)]
        public static void InternalPage()
        {
            Console.WriteLine("Go to http://www.mysite.com/control/internal");
        }
    }

    /// <summary>
    /// Custom attribute class.
    /// It has two properties are Authorized and Role
    /// </summary>
    internal class AccessAttribute : Attribute
    {
        public bool Authorized { get; set; }
        public Role[] Role { get; set; }

        public AccessAttribute()
        {
            Role = null;
        }

        /// <summary>
        /// Its constructor can hold many roles
        /// </summary>
        /// <param name="newAuthorized"></param>
        /// <param name="newRole"></param>
        public AccessAttribute(bool newAuthorized, params Role[] newRole)
        {
            Authorized = newAuthorized;

            if (Authorized)
            {
                Role = newRole;
            }
        }
    }

    /// <summary>
    /// Main class. It has Main function
    /// </summary>
    internal class Program
    {
        // Global user who are on website
        private static readonly User CurrentUser = new User("Edward", Role.Guest);

        private static void Main()
        {
            Console.WriteLine("User permission: Guest");
            // Change user permission
            CurrentUser.Role = Role.Guest;

            TryToAccessAllPage();

            Console.WriteLine("User permission: User");
            // Change user permission
            CurrentUser.Role = Role.User;

            TryToAccessAllPage();

            Console.WriteLine("User permission: Admin");
            // Change user permission
            CurrentUser.Role = Role.Admin;

            TryToAccessAllPage();

            Console.ReadKey();
        }

        /// <summary>
        /// Assume, a user is trying to access to all pages of website
        /// </summary>
        private static void TryToAccessAllPage()
        {
            Console.Write("Try to access to public page: ");
            // Validate permission of user
            if (Utility.ValidatePermission(CurrentUser.Role, typeof (WebSite),
                                            Utility.GetMethodName(WebSiteMethod.HomePage)))
            {
                WebSite.HomePage();
            }
            else
            {
                Console.WriteLine("Access denied");
            }

            Console.Write("Try to access to internal page: ");
            // Validate permission of user
            if (Utility.ValidatePermission(CurrentUser.Role, typeof (WebSite),
                                            Utility.GetMethodName(WebSiteMethod.InternalPage)))
            {
                WebSite.InternalPage();
            }
            else
            {
                Console.WriteLine("Access denied");
            }

            Console.Write("Try to access to admin page: ");
            // Validate permission of user
            if (Utility.ValidatePermission(CurrentUser.Role, typeof (WebSite),
                                            Utility.GetMethodName(WebSiteMethod.AdminPage)))
            {
                WebSite.AdminPage();
            }
            else
            {
                Console.WriteLine("Access denied");
            }
        }
    }
}

Happy coding!!

Advertisements

3 thoughts on “C# Tutorial – Tạo và sử dụng Custom Attribute”

  1. Chào bạn
    Cám ơn bài ví dụ của bạn.
    Mình có đôi chỗ không hiểu xin bạn chỉ giúp:

    public static bool ValidatePermission(Role role, Type classType, string methodName)
    {
    MethodInfo mi = classType.GetMethod(methodName);

    var attrs = mi.GetCustomAttributes(typeof (AccessAttribute), false);
    // Biến attrs dùng đề mục đích cụ thề là gì vậy bạn ?
    // Mình thử in nó thì ra kết quả là CustomAttribute.AccessAttribute []
    // Mình dùng câu lệnh foreach( var a in attrs) mục đích in nó nhưng không được.

    AccessAttribute req = attrs.Cast().FirstOrDefault();
    //Câu lệnh này mình không biết attrs ở trên chứa gì mà có thể cast được

    return (req == null) || ((req.Role.Contains(role)));
    //Ah cho mình hỏi thêm return như vầy nghĩa là gì nha bạn . Mình không biết có phải return a||b không ?

    }

    Chúc bạn khỏe mạnh.

    1. Mình xin sửa lại chỗ “// Biến attrs dùng đề mục đích cụ thề là gì vậy bạn ?” >—> Mình viết hơi khó hiểu ( xin lỗi bạn nhé :))
      Mình thấy bạn gọi phương thức GetCustomAttributes() từ đối tượng là MethodInfo . Mình không hiểu nó trả về cho biến attrs những gì. Vì nếu là đối tượgn gọi phương thức là Attribute thì nó trả về 1 mảng kiểu Attribute đúng không bạn ?

      1. Hi bạn, mình xin trả lời các câu hỏi của bạn
        Q1:
        var attrs = mi.GetCustomAttributes(typeof (AccessAttribute), false);
        // Biến attrs dùng đề mục đích cụ thề là gì vậy bạn ?
        A1: Trước tiên bạn phải để ý đến dòng: MethodInfo mi = classType.GetMethod(methodName);
        Mình đang get về info của 1 method nào đó. Ví dụ như method Website.AdminPage
        Sau đó mình sẽ get các custom attribute của method đó. Cụ thể là get ra lớp AccessAttribute ( [Access(true, Role.Admin)] )

        Q2:
        AccessAttribute req = attrs.Cast().FirstOrDefault();
        //Câu lệnh này mình không biết attrs ở trên chứa gì mà có thể cast được
        A2: Bạn có thể để ý biên attrs được get ở trên rõ ràng là từ AccessAttribute. Nên bạn phải cast nó sang AccessAttribute

        Q3:
        return (req == null) || ((req.Role.Contains(role)));
        //Ah cho mình hỏi thêm return như vầy nghĩa là gì nha bạn . Mình không biết có phải return a||b không ?
        A3: Chính xác là a || b đó bạn. Nếu method AdminPage (ví dụ thôi nhé) nó ko có bất kỳ custom attribute nào (nghĩa là bạn không có dòng [Access(true, Role.Admin)] ở trên) thì bạn có thể toàn quyền truy cập, nếu method đó có custom attribute thì Role của User phải là Role.Admin mới được truy cập.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s