Atributos com reflection

Figura 1: Atributos com reflection

Para aqueles que ainda não sabem ou até sabem, mas não tinham se dado conta, começamos este artigo com exemplos de atributos de classe.

Listagem 1: Um Atributo de Classe

[Obsolete]
public void Metodo();
[Serializable]
public class Classe();
[WebMethod]
public static void CallAjax();

Esses nomes entre chaves acima de cada nome de método ou declaração de classe são os atributos e servem para identificar e classificar classes e seus campos segundo alguns critérios, atribuindo-lhes propriedades específicas para utilização no contexto em que são utilizadas.

Por exemplo, o atributo Obsolete permite definir uma mensagem indicando que o método está obsoleto e indicando qual utilizar em seu lugar.

Então, assumindo que possuímos o conhecimento de reflexão, vamos partir para o entendimento de como criar atributos de classes.

Listagem 2: Criando um atributo simples

    [AttributeUsage(System.AttributeTargets.Method)]
    public class MethodAttribute : Attribute
    {
        private int _Variavel1 = 0;
        public string _Variavel2 = "";
        public MethodAttribute()
        {
        }
        public MethodAttribute(string metodo, string metodo1)
        {
        }
        public void Metodo()
        {
        }
        public void Metodo(string entrada)
        {
            
  }
} 

AttributeUsage()

Responsável pelas regras de como o atributo irá funcionar. Nele indicamos que o atributo servirá de assinaturas, para classes, métodos ou algumas outras opções encontradas na listagem 3.

Listagem 3: AttributeTargets Enum

public enum AttributeTargets
  {
      Assembly ,
      Module,
      ClasS,
      Struct,
      Enum,
      Constructor,
      Method,
      Property,
      Field,
      Event,
      Interface,
      Parameter,
      Delegate,
      ReturnValue,
      GenericParameter,
      All = GenericParameter | ReturnValue | Delegate | Parameter | Interface | Event | Field | Property | Method | Constructor | Enum | Struct | Class | Module | Assembly
  }

Cada opção dessas habilita uma possibilidade diferente para o atributo gerado. Além do AttributeTargets, existem outros dois parâmetros dentro de AttributeUsage, são eles:

  • AllowMultiple: Habilita que o atributo seja assinado mais de uma vez, dentro do escopo definido para o mesmo, o valor padrão é false.
  • Inherited: Habilita que as classes derivadas herdem o atributo também, o valor padrão é true.

Muito rápido e simples, porém terá um grande ganho de automatização quando tudo estiver programado.

A Estrutura

A estrutura proposta para esse sistema é feita em 3 camadas e uma camada de reflexão. A ideia da estrutura não está no foco.

Estrutura da aplicação

Figura 2: Estrutura da aplicação

Repare que o Reflection é o ultimo passo, pois ele será o responsável (neste caso) por efetuar o envio das informações, por isso foi dito que a estrutura não é o foco, pois a DAL e até mesmo a Bussines, neste caso, simplesmente passam as informação para a próxima camada, não realizam tarefas.

Codificando os Atributos

Os atributos é a parte mais importante daqui e fácil de desenvolver. Os atributos são basicamente classes com construtores (não obrigatoriamente) e propriedades.

Existem atributos mais complexos que realizam tarefas de classes robustas, mas na grande maioria os atributos serão simples.

Toda classe de atributo herda de Attribute e a mesma é assinada com AttributeUsage com as definições que foram explicadas acima.

Para este exemplo, foram criados dois atributos, um se chama StoredProcedureAttributes e o outro FieldsAttributes.

Listagem 4: Codificando os atributos

public enum ProcedureTypeCommand
    {
        Insert,
        Delete,
        Update
    }

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
    public class StoredProcedureAttributes : Attribute
    {
        public string ProcedureName { get; set; }
        public ProcedureTypeCommand ProcedureType { get; set; }
        public String[] ProcParams { get; set; }

        public StoredProcedureAttributes(string procName, ProcedureTypeCommand procType, params String[] param)
        {
            ProcedureName = procName;
            ProcedureType = procType;
            ProcParams = param;
        }
    }

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public class FieldsAttributes : Attribute
    {
        public string FieldName { get; set; }
        public SqlDbType FieldType { get; set; }
        public int Length { get; set; }
        public object Value { get; set; }

        public FieldsAttributes()
        {
        }
        public FieldsAttributes(string FieldName, SqlDbType FieldType)
        {
            this.FieldName = FieldName;
            this.FieldType = FieldType;
        }
        public FieldsAttributes(string FieldName, SqlDbType FieldType, int Length)
        {
            this.FieldName = FieldName;
            this.FieldType = FieldType;
            this.Length = Length;
        }
    }

Simples assim foram definidos dois atributos, um é assinatura de método e o outro como assinatura de propriedades. Caso tente colocar um atributo definido para propriedade em uma classe, por exemplo, o interpretador e o compilador Visual Studio irá exibir um erro.

O atributo StoredProcedureAttributes, recebe os dados do Procedure do banco de dados, o tipo de evento DML que ele realiza e os parâmetros existentes no procedure (obrigatórios ou não).

O atributo FieldsAttributes recebe o nome do Parâmetro correspondente na entrada do procedure, o tipo de dado que a coluna foi definida no banco de dados e o length (para Varchar, Char e etc) para definir o tamanho da informação máxima que o parâmetro pode conter.

Codificando as Camadas

As camadas não tem mistério, com exceção da camada de Entity. Nesta camada estão todas as assinaturas dos atributos e é por ela que o Reflection consegue definir o tipo de ação a ser realizada e os parâmetros passados.

Entity

A Entity é um espelho da tabela de dados e possui as regras de “DML” amarradas a ela. Nesta camada estão as definições das assinaturas.

Listagem 5: Entidade e os atributos

    public class Titulos
    {
        #region Propriedades
        private int _id;
        private string _nome;
        private int _idCategoria;
        private string _categoria;
        private string _duracaoFilme;

        [FieldsAttributes("@ID", System.Data.SqlDbType.Int)]
        public int ID
        {
            get { return _id; }
            set { _id = value; }
        }

        [FieldsAttributes("@NOME", System.Data.SqlDbType.VarChar, 30)]
        public string Nome
        {
            get { return _nome; }
            set { _nome = value; }
        }
        [FieldsAttributes("@IDCATEGORIA", System.Data.SqlDbType.Int)]
        public int IdCategoria
        {
            get { return _idCategoria; }
            set { _idCategoria = value; }
        }

       [FieldsAttributes("@CATEGORIA", System.Data.SqlDbType.VarChar, 30)]
        public string Categoria
        {
            get { return _categoria; }
            set { _categoria = value; }
        }
        [FieldsAttributes("@DURACAOFILME", System.Data.SqlDbType.VarChar, 20)]
        public string DuracaoFilme
        {
            get { return _duracaoFilme; }
            set { _duracaoFilme = value; }
        }
        #endregion
        private TitulosBLL titulosBLL;
        public Titulos()
        {
            titulosBLL = new TitulosBLL();
        }

        #region Metodos Assinados
        [StoredProcedureAttributes("STP_INS_TITULOS", ProcedureTypeCommand.Insert, "@NOME", "@IDCATEGORIA", "@CATEGORIA", "@DURACAOFILME")]
        public void Salvar()
        {
            titulosBLL.Salvar(this);
        }
        [StoredProcedureAttributes("STP_DEL_TITULOS", ProcedureTypeCommand.Delete, "@ID")]
        public void Deletar()
        {
            titulosBLL.Deletar(this);
        }
        #endregion
    }

Veja como ficaram os atributos criados, em cima das propriedades apenas o atributo para propriedade e o mesmo para os métodos. Toda a declaração que seria feita na camada de DAL foi transportado para a entidade, isso é um exemplo para a funcionalidade do Reflection.

Observação: Muitos devotos por patterns e outros, com certeza não aprovariam um modelo desses.

Camada de Reflection

A última camada do escopo realiza a reflexão do objeto passado pela entidade e captura as demais informações para realizar a ação em questão (salvar, deletar).

Primeiramente vamos entender alguns métodos de reflexão dessa classe.

O GetFieldsAttributes é um método responsável por capturar do objeto passado para a camada todas as propriedades as quais levam a assinatura definida anteriormente.

Listagem 6: Código do GetFieldsAttributes

private void GetFieldsAttributes(System.Type type, object obj)
        {
            ListFields.Clear();
            foreach (var propertyInfo in type.GetProperties())
            {
                foreach (Attributes.FieldsAttributes customAttribute in propertyInfo.GetCustomAttributes(typeof(Attributes.FieldsAttributes), false))
                {
                    customAttribute.Value = propertyInfo.GetValue(obj, null);
                    ListFields.Add(customAttribute);
                }
            }
        }

O GetStoredProcedureAttributes, por sua vez, é o método responsável por capturar do objeto passado para a camada todas as propriedades as quais levam a assinatura de StoredProcedureAttributes.

Listagem 7: GetStoredProcedureAttributes

private void GetStoredProcedureAttributes(Type type)
        {
            listProcMethods.Clear();
            foreach (var methodInfo in type.GetMethods())
            {
                foreach (Attributes.StoredProcedureAttributes storedProcedureAttributes in methodInfo.GetCustomAttributes(typeof(Attributes.StoredProcedureAttributes), false))
                {
                    listProcMethods.Add(storedProcedureAttributes);
                }
            }
        }

Eles juntam as informações em listas distintas definidas na classe como static. Essas listas de informações são necessárias para a filtragem dos parâmetros corretos na hora do envio das informações.

O evento Salvar recebe uma variável genérica para permitir receber qualquer objeto dentro do escopo definido.

Sem mais delongas, segue:

Listagem 8: Método Salvar

   public void Salvar<T>(T obj)
        {
            var type = typeof(T);
            GetFieldsAttributes(type, obj);
            GetStoredProcedureAttributes(type);

            Attributes.StoredProcedureAttributes StoredProcedure = listProcMethods.First(p => p.ProcedureType == Attributes.ProcedureTypeCommand.Insert);
            Salvar(StoredProcedure);
        }
        private void Salvar(Attributes.StoredProcedureAttributes storedProcSalvar)
        {
            SqlCommand cmd = new SqlCommand();
            try
            {
                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                cmd.CommandText = storedProcSalvar.ProcedureName;
                cmd.Connection = GetConnection();
                cmd.Parameters.Clear();

                foreach (string procParam in storedProcSalvar.ProcParams)
                {
                    var field = ListFields.First(whe => whe.FieldName == procParam);
                    cmd.Parameters.Add(new SqlParameter(field.FieldName, field.FieldType, field.Length) { Value = field.Value });
                }
                cmd.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                cmd.Connection.Close();
            }
        }

Observação: Na linha onde é feito o First para definir qual o procedure deve ser utilizado, poderia ser feito de outra forma, utilizando atributos específicos para cada evento.

Veja como é simples e reutilizável tudo isso. A reflection é capaz de ler os atributos da classe e os valores das propriedades definidos no objeto, minerando toda essa informação e facilitando na automatização do processo de envio.

Essa camada, nesse exemplo, entende os parâmetros que o procedure receberá e e dentro do foreach é feita a separação do que é preciso para a chamada do procedure.

Conclusão

Os atributos são uma vantagem para as aplicações, com eles conseguimos automatizar processos rotineiros, encapsular e reutilizar tarefas repetitivas para todos os desenvolvedores de uma célula. Neste caso do CRUD facilitará em constantes acessos ao banco de dados e em termos de produtividade gera um ganho, pois é possível reutilizar os atributos e a classe de reflexão em qualquer projeto.

Portanto pesquisem mais ainda sobre o assunto. Baixem o código fonte para ver o funcionamento!

Foi disponibilizado uma base dentro do App_Data, caso a mesma falhe, utilize o script de banco de dados e stored procedures disponibilizado dentro do arquivo zip, não se esqueça de editar o web.config para o endereço do banco corretamente.

Obrigado.

Referência