4

Filtros en ASP.NET MVC

En ASP.NET MVC los controladores (mediante sus métodos de acción) son los encargados de ejercer como intermediarios entre el usuario y el sistema.  Cuando por ejemplo se pulsa un botón, se envía un formulario o se introduce una dirección url en el navegador, se genera una petición al servidor y el framework mediante el módulo de rutas (UrlRoutingModule) analiza la url y la redirecciona al controlador y método de acción adecuado, que la gestiona y devuelve el resultado.

Los filtros de ASP.NET MVC nos permiten mediante atributos agregar comportamientos previos y posteriores a los métodos de acción de los controladores. Es decir, nos van a permitir ejecutar código antes y/o después de que se ejecute una acción en el controlador. En la imagen anterior podemos ver dentro de la Pipeline de ASP.NET MVC (en la imagen la he resumido) donde y cuando se ejecuta cada filtro. Podemos crear tantos filtros como sean necesarios ya sea implementando las interfaces en nuestras clases, heredando de los filtros ya existentes o de clases abstractas ya creadas en el framework.

Un filtro de asp.net es un atributo que normalmente implementa la clase abstracta FilterAttribute. Estos atributos pueden asociarse a un método de acción de un controlador,  al controlador directamente para que todas las acciones se asocien al filtro, o asociarlo a nivel global de la aplicación para que todos los métodos de acción del proyecto se vean ajustados.

ASP.NET MVC nos proporciona los siguientes tipos de filtros:

Filtro de autenticación

Este tipo de filtros son nuevos en ASP.NET MVC 5 y vienen a complementar a los filtros de autorización. Básicamente han sido creados con el objetivo de separar responsabilidades con los filtros de autorización. El filtro de autenticación se responsabilizará de validar si el usuario es válido e incluirlo en la petición, mientras que el filtro de autorización se encargará de comprobar si el usuario puede o no ejecutar una acción. Este tipo de filtro y más concretamente su método OnAuthentication se ejecuta antes que cualquier otro filtro. Implementan la interfaz IAuthenticationFilter 

public interface IAuthenticationFilter
{
    void OnAuthentication(AuthenticationContext filterContext);
    void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext);
}

Podemos observar que se declaran dos métodos:

OnAuthentication: Es el método que se ejecutará antes de cualquier acción o filtro. Su objetivo es el de autenticar la solicitud, como por ejemplo inicializando el principal o modificándolo.

OnAuthenticationChallenge: Este método nos puede servir por ejemplo para añadir tareas adicionales a la solicitud  o redirigir al usuario a otra página. La idea es que el método puede utilizarse para contribuir al resultado, por ejemplo para convertir un HttpUnauthorizedResult en una redirección a otra página de conexión según cierta lógica, añadir una sesión en un momento determinado, etc.

Es  importante recalcar que este método no necesariamente tiene que ejecutarse justo después de OnAuthentication sino que según el contexto de la solicitud puede hacerlo después de la acción y de otros tipos de filtros.. Si no hicieramos nada en el OnAuthentication seguidamente se ejecutaría la acción o algún otro de sus filtros, pero si por ejemplo en el OnAuthentication modificamos el result enviando un HttpUnauthorizedResult (indicando una petición no autorizada), estaríamos impediedo la ejecución de cualquier acción y de sus correspondientes filtros y en este caso después de OnAuthentication se ejecutaría OnAuthenticacionChallenge.

public void OnAuthentication(AuthenticationContext filterContext)
{
    if (!filterContext.Principal.Identity.IsAuthenticated)
        filterContext.Result = new HttpUnauthorizedResult();
}

Filtro de autorización:

Como ya he adelantado, su objetivo es la toma decisiones de seguridad para restringir el acceso a las acciones. AuthorizeAttribute es un ejemplo de filtro de autorización. Este tipo de filtros se ejecutan después del filtro de autenticación e implementan la interfaz IAuthorizationFilter .

public interface IAuthorizationFilter
{
    void OnAuthorization(AuthorizationContext filterContext);
}

La interfaz declara un método:

OnAuthorization: Se ejecutará cuando se necesite una autorización. Aquí podemos determinar que permisos tiene el usuario y si se le permitirá por ejemplo acceder o ejecutar determinadas acciones. Todos sabemos que un usuario puede tener permisos para modificar un registro mientras que otro puede tener sólo permisos de lectura.

Filtro de acción:

Añade procesamiento adicional a la acción haciendo posible agregar comportamientos antes o después de que la acción se ejecute. Se usan para multitud de escenarios, añadir logs, tareas de localización e internalización de idioma como es el determinar  y establecer el idioma de la aplicacion( culture/locale), modificar el comportamiento de la aplicación en función del agente de usuario del navegador, añadir restricciones para “Anti-image-leeching” con el objetivo de negar imágenes a peticiones que no son de tu web,  injectar acciones dinámicas al controlador y muchas cosas más. Estos filtros implementan la interfaz IActionFilter.

public interface IActionFilter
{
    void OnActionExecuted(ActionExecutedContext filterContext);
    void OnActionExecuting(ActionExecutingContext filterContext);
}

Como vemos, la interfaz declara dos métodos:

OnActionExecuted: Se llama a este método después de ejecutar una acción.

OnActionExecuting: Se llama a este método antes de ejecutar una acción.

Filtro de resultados:

Añaden procesamiento adicional a los resultados de la acción (ActionResult), como por ejemplo la modificación de la vista o resultados,registro de logs, modificar el modelo, redirigir a una página de error si detectamos que el modelo no es valido, etc. La clase OutputCacheAttribute es un ejemplo de filtro de resultado. Son parecidos a los filtros de acción pero en este caso la acción ya ha sido ejecutada. Estos tipos de filtro implementan la interfaz IResultFilter.

public interface IResultFilter
{
    void OnResultExecuted(ResultExecutedContext filterContext);
    void OnResultExecuting(ResultExecutingContext filterContext);
}

Podemos ver que la interfaz declara dos métodos:

OnResultExecuted: Se ejecuta inmediatamente después de que el resultado (ActionResult ) ha sido ejecutado.

OnResultExecuting: Se ejecuta justo antes de invocar a la instancia/objeto ActionResult .

Filtro de excepciones:

Se ejecutan cuando se inicia una excepción no controlada en el método de acción o en alguno de sus filtros. Se suelen utilizar para registrar logs, tratar excepciones y/o mostrar páginas de error. HandleErrorAttribute es un buen ejemplo de este tipo de filtros. Los filtros de excepción implementan la interface IExceptionFilter

public interface IExceptionFilter
{
    void OnException(ExceptionContext filterContext);
}

La interfaz define un único método:

OnException: Es llamado cuando se produce una excepción.

La clase Controller de ASP.NET MVC

Voy ha hacer un parentisis puesto que aquí nos topamos con una de las características de ASP.NET MVC y no es otra que el número de enfoques que se pueden tomar para hacer algo.

Todas las interfaces nombradas anteriormente son implementadas por el Controller, por lo que de hecho nos bastaría con sobreescribir alguno de los métodos de estas interfaces en el propio controlador para introducir lógica antes y después de las acciones, al producirse una excepción, etc..

Podemos sobreescribir uno de los métodos, por ejemplo podríamos sobreesribir en el controlador el OnActionExecuting para hacer un log y registrar el nombre de la acción que se va a ejecutar. Pero donde dejaríamos la separación de responsabilidades?. Es más, ese código ya no podríamos reutilizarlo a no ser que crearamos un controlador base y luego hacer que heredaran de él los demás controladores. El problema es que con este enfoque nos podríamos ver en la situación de que alguien (o nosotros mismos) sobreescribirá también el método en otro controlador y se olvidara de llamar al método base. Podriamos aceptar barco si realmente es algo muy característico del controlador, esto ya es decisión de cada programador y la situación que se encuentre. Mi opinión general es que siempre es mas recomendable usar un filtro por atributos que sobrescribir los método en el controlador. Si lo incluimos en el controlador base vamos a tener la seguridad de que será ejecutado.

Filtros definidos en ASP.NET MVC

En ASP.NET MVC podemos crear nuestros propios filtros o usar los ya desarrollados en el framework. Algunos de estos filtros son los siguientes:

Authorize:

Al marcar una acción con este filtro estaremos indicando que el acceso a esa acción esta restringido a los usuarios. Si lo añadimos al controlador todos los métodos de acción de ese controlador estarán restringidos y si lo ponemos a nivel global todos los filtros de la aplicación quedarán restringidos. Por defecto si sólo ponemos el filtro sin indicar nada más, los usuarios que no hagan login no tendrán acceso, pero también podemos indicar cuales serán los  usuarios o roles que tendrán acceso a ese método.

public class HomeController : Controller
 {
     public ActionResult Index()
     {
         ViewData["Message"] = "Bienvenido";

         return View();
     }

     public ActionResult AllUsers()
     {
         return View();
     }

     [Authorize]
     public ActionResult AuthenticatedUsers()
     {
         return View();
     }

     [Authorize(Roles = "Admin")]
     public ActionResult AdministratorsOnly()
     {
         return View();
     }

     [Authorize(Users = "Jose, Izan")]
     public ActionResult SpecificUserOnly()
     {
         return View();
     }
 }

En el ejemplo anterior, cualquier usuario anónimo tendrá acceso a la acción AllUsers. Para la acción AdministratorsOnly únicamente tendrán acceso aquellos usuarios con el rol “Admin”, mientras que la acción SpecificUserOnly únicamente estará disponible para los usuarios Jose e Izan.

Un atributo muy útil para usar junto a Authorize es AllowAnonymous. Si por ejemplo pusiéramos a nivel global de la aplicación el filtro Authorize, todos nuestros métodos de acción estarián restringidos. Si necesitamos que una acción en concreto sea accesible para los usuarios anónimos la decoraremos con el atributo AllowAnonymous.

Mas información en AuthorizeAttribute

ChildActionOnly:

Este filtro nos permitirá restringir el acceso a las acciones para que sólo puedan ser llamadas desde nuestra aplicación y no desde fuera. Si decoramos un método de acción con este filtro y por ejemplo  intentamos acceder a él  indicando la dirección en la barra url del navegador veremos que no tenemos acceso a él. En la práctica al poner este filtro estamos restringiendo el uso de la acción a llamadas dentro de nuestra aplicación mediante los métodos de extensión Action y RenderAction. Además las acciones marcadas con este filtro sólo deberían devolver vistas parciales.

Más información en ChildActionOnlyAttribute

OutputCache:

Nos servirá para almacenar el resultado de un método de acción en la memoria caché. Pondré un ejemplo de este filtro más adelante.

Más información en OutputCacheAttribute

HandleError:

Se utiliza para controlar una excepción en un método de acción. Por defecto si se produce un error en la aplicación, ASP.NET redirigirá a la vista Error situada en Views/shared de nuestro proyecto. Podemos cambiar este coportamiento predeterminado de este filtro ajustando varias de sus propiedades.

ExceptionType: Para indicar los tipos de excepción que controlará el filtro.

View: Nombre de vista que utilizará el filtro para mostrar el error.

Master: Especifica el nombre de la vista maestra que se va a utilizar, si es que vamos a utilizar alguna vista maestra.

Order: Orden en el que se aplican los filtros. Vamos a hablar sobre esto más adelante en este artículo.

Para habilitar el uso de los errores personalizados hay que añadir el elemento customErrors a system.web del archivo web.config.

<system.web>;
  <customErrors mode="On" defaultRedirect="Error" />
</system.web>;

Mas información en HandleErrorAttribute

ValidateAntiForgeryToken:

Este filtro nos ayudará a defendernos contra la falsificación de entidades entre sitios. Podemos añadir en nuestro formulario el helper AntiForgeryToken, que generará un token dentro de un campo oculto, por otro lado decoramos nuestro método de acción para el post del formulario con el filtro ValidateAntiForgeryToken. Cuando el usuario rellene el formulario y envie la petición (submit), ASP.NET MVC comprobará que el token enviado es correcto. Esto lo hara comporando una cookie que habrá creado con el token contra el campo de formulario.

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    ...
}
[ValidateAntiForgeryToken]
public ActionResult SubmitPost(ViewModel vm)
{
    ...
}

Más información en ValidateAntiForgeryTokenAttribute

RequireHttps:

El uso de este filtro obligará a que la llamada a la acción se haga mediante el protocolo HTTPS.

Más información en RequireHttpsAttribute

Como usar los filtros:

Anteriormente hemos visto que un filtro es una clase que hereda de FilterAttribute, que a su vez hereda de System.Attribute. Los filtros puedes utilizarse a nivel de acción, de controlador o bien a nivel global. Por ejemplo, si quisiéramos utilizar un filtro en una acción, nos bastaría con decorar el método con el atributo. En el siguiente ejemplo se usa un filtro ya existente, como es OutputCache.

[OutputCache(Duration=20)]
public ActionResult Index()
{
    ViewBag.Time = DateTime.Now.ToLongTimeString();
    return View();
}

El código anterior estaría cacheando en memoria la respuesta de la acción cada 20 segundos. Si mostramos la variable Time del ViewBag en la página, y la actualizamos antes de esos 20 segundos, observariamos que los segundos no se han actualizado. Pasado ese tiempo vuelve a ejecutar la acción y la cachea viendo actualizado el tiempo. Es un ejemplo un poco simple pero sirve para ver como se utiliza un filtro en una acción en concreto. Un uso útil de OutputCache podría ser el de cachear la salida de una lista de datos fijos o que no se ven alterados continuamente.. por ejemplo un listado de países o de categorías.

Otra manera de usar un filtro es decorando directamente el controlador con el atributo, de esta manera todas las acciones verían cacheado su resultado.

[OutputCache(Duration = 20)]
public class HomeController : Controller
{
    ...

}

Por último y no menos importante, podemos usar los filtros de manera global en nuestro proyecto y que se ejecuten siempre en cada llamada de acción a nuestro sistema. Para ello y en versiones de MVC 4 o superiores lo correcto será editar el archivo FilterConfig.cs situado en la carpeta App_Start de nuestro proyecto ASP.NET MVC y añadir el filtro a la colección en el método estático RegisterGlobalFilters.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new OutputCache(Duration = 20));
        filters.Add(new AuthorizeAttribute());
    }
}

En versiones de MVC 3 y anteriores no tendremos el archivo FilterConfig.cs, de hecho no encontraremos en el proyecto ni la carpeta App_Start. En estos casos el lugar donde añadiremos los filtros será en el método Application_Start() del archivo Global.asax

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    // Register global filter
    GlobalFilters.Filters.Add(new OutputCache(Duration = 20));

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}

Orden de ejecución de los filtros:

Los filtros siguen el siguiente orden de ejecución según su tipo.

  1. Filtros de autenticación.
  2. Filtros de autorización
  3. Filtros de acción
  4. Filtros de resultado.
  5. Filtros de excepción

Este orden es inamovible, ahora bien, dentro de cada tipo podemos definir un orden. Todos los filtros tienen una propiedad de tipo entero llamada Order (heredada de FilterAttribute)  que podemos utilizar para establecer el orden de ejecución entre filtros de un mismo tipo. Si la propiedad se omite tomará el valor de -1. El tipo de ordenacion es ascendente, cuanto menor sea el número antes se ejecutará. Por ejemplo, si definimos tres filtros de autorización en una acción del controlador.

public class HomeController : Controller
{
    [AuthorizationFilterA(Order=3)]
    [AuthorizationFilterB(Order=1)]
    [AuthorizationFilterC(Order=2)]
    public ActionResult Index()
    {
        return View();
    }
}

El orden de ejecución, como ya podéis preveer será:

  1. AuthorizationFilterB
  2. AuthorizationFilterC
  3. AuthorizationFilterA

Si en cambio tenemos varios tipos de filtros, la propiedad Order actuará entre los filtros de su mismo tipo.

public class HomeController : Controller
{
    [ActionFilterB(Order=3)]
    [ActionFilterA(Order=1)]
    [AuthorizationFilterA(Order=2)]
    [AuthorizationFilterB]
    public ActionResult Index()
    {
        return View();
    }
}

El orden será el siguiente:

  1. AuthorizationFilterB
  2. AuthorizationFilterA
  3. ActionFilterA
  4. ActionFilterB

AuthorizationFilterB se ejecuta primero porque no hemos indicado ningún orden para él y por tanto el valor para la propiedad Order se establece por defecto en -1.

Hay otro tipo de orden prederterminado que mvc utiliza para los filtros, y es el del ámbito o alcance del filtro. El ámbito o alcance del filtro es en la práctica donde esta declarado. Internamente mvc utiliza la enumeración FilterScope

namespace System.Web.Mvc
{
    public enum FilterScope
    {
        First = 0,
        Global = 10,
        Controller = 20,
        Action = 30,
        Last = 100
    }
}

La lógica de ordenacion es la misma, se ejecutaran antes los filtros con el valor de enumeración mas bajo. Si nos fijamos en la enumeración, podemos decir que los filtros en un ámbito global se ejecutarán antes que los de ambito de controlador y estos a su vez se ejecutarán antes que los filtros en ámbito de acción. Un ejemplo sencillo para verlo.

[ActionFilterA]
public class HomeController : Controller
{
    [ActionFilterB]
    public ActionResult Index()
    {
        return View();
    }
}

… por otro lado definimos en ámbito global otro filtro de acción.

GlobalFilters.Filters.Add(new ActionFilterC());

El orden de ejecución quedará de esta manera:

  1. ActionFilterC
  2. ActionFilterA
  3. ActionFilterB

Para aclarar y como resumen, el orden de los filtros se basa primero en el tipo de filtro al que pertenece, luego por su propiedad Order y finalmente por su ámbito. Por tanto, la propiedad Order no variará el orden entre tipos pero si tendrá prioridad sobre el ámbito.

Mismo ejemplo anterior pero añadiendo la propiedad order:

[ActionFilterA(Order=2)]
public class HomeController : Controller
{
    [ActionFilterB(Order=1)]
    public ActionResult Index()
    {
        return View();
    }
}

… definimos en ámbito global otro filtro de acción.

GlobalFilters.Filters.Add(new ActionFilterC(){Order=3});

Orden de ejecución resultante:

  1. ActionFilterA (Action)
  2. ActionFilterB (Controller)
  3. ActionFilterC (Global)

Si dos filtros del mismo tipo tienen un mismo valor de Order y ámbito, su orden de ejecución se establecerá en indefinido,  y su orden real será el orden en el que se añadió a la lista de filtros. Por otra parte la propiedad order no acepta valores inferiores a -1, si lo hacemos nos lanzará una excepción en tiempo de ejecución.

Hay otra particularidad en la ejecución del orden de los filtros. Como hemos ido viendo algunos filtros tienen más de un método y algunos métodos se ejecutan en orden hacia adelante y otros en orden inverso. Esto es más fácil explicarlo con un ejemplo.

[AuthorizationFilterB]
[ActionFilterB]
[ResultFilterB]
public class HomeController : Controller
{
    [AuthorizationFilterC]
    [ActionFilterC]
    [ResultFilterC]
    public ActionResult Index()
    {
        return View();
    }
}

… por otro lado definimos en ámbito global otro filtro de acción.

GlobalFilters.Filters.Add(new AuthorizationFilterA());
GlobalFilters.Filters.Add(new ActionFilterA());
GlobalFilters.Filters.Add(new ResultFilterA());

Orden de ejecución:

  1. AuthorizationFilterA – OnAuthorization (Global) – orden Adelante
  2. AuthorizationFilterB – OnAuthorization(Controller)  – orden Adelante
  3. AuthorizationFilterC – OnAuthorization(Action) – orden Adelante
  4. ActionFilterA – OnActioExecuting (Global) – orden Adelante
  5. ActionFilterB – OnActioExecuting (Controller) – orden Adelante
  6. ActionFilterC – OnActioExecuting (Action) – orden Adelante

  7. Home Controller, Index Action

  8. ActionFilterC – OnActionExecuted (Action) – orden Inverso
  9. ActionFilterB – OnActionExecuted (Controller) – orden Inverso
10. ActionFilterA – OnActionExecuted (Global) – orden Inverso
11. ResultFilterA – OnResultExecuting (Global) – orden Adelante
12. ResultFilterB – OnResultExecuting (Controller) – orden Adelante
13. ResultFilterC – OnResultExecuting (Action) – orden Adelante
14. ResultFilterC – OnResultExecuting (Action) – orden Inverso
15. ResultFilterB – OnResultExecuting (Controller) – orden Inverso
16. ResultFilterA – OnResultExecuting (Global) – orden Inverso

 

Crear filtros personalizados:

Como he mencionado anteriormente o muy anteriormente visto el mamotreto en que se ha ido convirtiendo el artículo, para crear un filtro nos basta o bien con implementar una de las interfaces existentes o heredar de una clase ya creada. Sabemos las interfaces y clases que existen, las hemos ido viendo a lo largo del artículo. Según el tipo de filtro que queramos crear usaremos una u otra interfaz. Así mismo sabemos que para definir cualquier tipo de filtro tenemos la clase base Abstracta FilterAttribute. Con estos datos ya podemos crear nuestro primer filtro. Vamos a crear un filtro de acción muy sencillo que escriba en la ventana de salida del IDE de visual studio el paso por los diferentes métodos del filtro.

public class ActionFilter : FilterAttribute, IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuted");
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuting");
    }
}

 

Si queremos que además se registren los eventos de un filtro de resultado en el mismo filtro nos bastaría con añadir e implementar la interfaz de los filtros de resultado.

public class ActionFilter : FilterAttribute, IActionFilter, IResultFilter
{
    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuted");
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuting");
    }

    public void OnResultExecuted(ResultExecutedContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnResultExecuted");
    }

    public void OnResultExecuting(ResultExecutingContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnResultExecuting");
    }
}

Esta es una de las maneras de crear un filtro personalizado. Hay otras como por ejemplo heredar de una clase ya existente. Por ejemplo existe la clase abstracta ActionFilterAttribute. Esta clase implementa exactamente las mismas interfaces que hemos utilizado anteriormente. Este es su código:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
    protected ActionFilterAttribute()
    {
    }

    public virtual void OnActionExecuted(ActionExecutedContext filterContext)
    {
    }

    public virtual void OnActionExecuting(ActionExecutingContext filterContext)
    {
    }

    public virtual void OnResultExecuted(ResultExecutedContext filterContext)
    {
    }

    public virtual void OnResultExecuting(ResultExecutingContext filterContext)
    {
    }
}

Como podemos ver, la clase hereda de FilterAttribute que como ya sabeis es la clase de la que heredan todos los filtros de asp.net mvc. Además implementa dos interfaces: IActionFilter y IResultFilter. Sabiendo ya esto.. nos basta con heredar de ella y sobreescribir los métodos para que se comporte exactamente igual que nuestro anterior filtro.

public class ActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuted");
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnActionExecuting");
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnResultExecuted");
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        Debug.WriteLine("ActionFilter: OnResultExecuting");
    }
}

4 Comments

  1. Excelente artículo, muy bien explicado.
    Creo que de pocos artículos donde puedes encontrar información como esta en un mismo lugar.
    Muchas felicidades

  2. Mejor explicado imposible!!! Despeje michas de las dudas que tenia. Gracias por compartir el conocimiento…no cualquiera

Responder a Alex Millan Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *