Proveedor de notificaciones personalizado Azure para el proyecto de SharePoint Project: parte 2
Artículo original publicado el miércoles, 15 de febrero de 2012
En la parte 1 de esta serie, describí brevemente los objetivos de este proyecto, que en un nivel elevado es usar el almacenamiento de tablas de Windows Azure como almacén de datos para un proveedor de notificaciones personalizado de SharePoint. El proveedor de notificaciones usará el CASI Kit para recuperar los datos que necesita de Windows Azure para proporcionar la funcionalidad de selector de personas (por ejemplo, libreta de direcciones) y resolución de nombres de tipo en control.
En la parte 3 creo todos los componentes que se usan en la granja de servidores de SharePoint. Eso incluye un componente personalizado basado en el CASI Kit que administra todas las comunicaciones entre SharePoint y Azure. Existe un elemento web personalizado que captura información acerca de nuevos usuarios y la inserta en una cola de Azure. Por último, hay un proveedor de notificaciones personalizado que se comunica con el almacenamiento de tablas de Azure a través de WCF (mediante el componente personalizado del CASI Kit) para permitir la funcionalidad del tipo en control y selector de personas.
Ampliemos ahora este escenario un poco más.
Este tipo de solución se incorpora muy bien en los escenarios comunes, es decir, cuando desee una extranet que no requiere mucha administración. Entonces, por ejemplo, quiere que sus socios o clientes puedan alcanzar un sitio web suyo, solicitar una cuenta y luego poder "aprovisionar" automáticamente dicha cuenta... donde "aprovisionar" puede significar varias cosas diferentes para personas distintas. Usaremos esto como la base del escenario aquí, pero evidentemente, dejaremos que nuestros recursos públicos en la nube hagan parte del trabajo.
Comencemos por echar un vistazo a los componentes de la nube que vamos a desarrollar nosotros mismos:
- Una tabla para controlar todos los tipos de notificaciones que vamos a admitir
- Una tabla para controlar todos los valores de notificación únicos para el selector de personas
- Una cola en la que podemos enviar datos que deberían agregarse a la lista de valores de notificación únicos
- Algunas clases de acceso a datos para la lectura y escritura de datos desde las tablas de Azure, y para la escritura de datos en la cola
- Un rol de trabajo de Azure que realizará la lectura de los datos desde la cola y poblará la tabla de valores de notificación únicos
- Una aplicación WCF que será el extremo a través del que la granja de servidores de SharePoint se comunica para obtener la lista de tipos de notificación, buscar notificaciones, resolver una notificación y agregar datos a la cola
Ahora miraremos a cada uno con más detalle.
Tabla de tipos de notificación
La tabla de tipos de notificaciones es donde se almacenarán todos los tipos de notificaciones que los proveedores de notificaciones personalizados podrán usar. En este escenario, solo usaremos un tipo de notificación, la dirección de correo electrónico. Se pueden usar otras notificaciones, pero para simplificar el escenario, solo se usará esta. En el almacenamiento de tablas de Azure se agregan las instancias de clases a una tabla, por lo que hay que crear una clase para describir los tipos de notificación. Nuevamente, tenga en cuenta que en Azure se pueden crear instancias de distintos tipos de clases en la misma tabla, pero para no complicar las cosas no lo haremos aquí. La clase que esta tabla usará tendrá el aspecto siguiente:
namespace AzureClaimsData
{
public class ClaimType : TableServiceEntity
{
public string ClaimTypeName { get; set; }
public string FriendlyName { get; set; }
public ClaimType() { }
public ClaimType(string ClaimTypeName, string FriendlyName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimTypeName);
this.RowKey = FriendlyName;
this.ClaimTypeName = ClaimTypeName;
this.FriendlyName = FriendlyName;
}
}
}
No cubriré todos los aspectos básicos de cómo trabajar con el almacenamiento de tablas de Azure porque existe una gran cantidad de recursos al respecto. Por tanto, si desea obtener más información acerca de lo que son los parámetros PartitionKey o RowKey y cómo usarlos, su motor de búsqueda Bing puede ayudarle. Lo que sí vale la pena mencionar aquí es que codificaré en formato URL el valor que almaceno para el parámetro PartitionKey. ¿Por qué? Porque en este caso, el valor PartitionKey es el tipo de notificación, que puede tener varios formatos: urn:foo:blah, https://www.foo.com/blah, etc. En el caso de un tipo de notificación que incluya barras diagonales, Azure no puede almacenar el parámetro PartitionKey con esos valores. Entonces, en cambio se codifican en un formato legible que se pueda usar en Azure. Como mencioné anteriormente, en nuestro caso usaremos la notificación de correo electrónico, por lo que el tipo de notificación es https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
Tabla de valores de notificación únicos
La tabla de valores de notificación únicos contiene todos los valores de notificación únicos que almacenamos. En nuestro caso, solo almacenamos un tipo de notificación (la notificación de identidad), por lo que, por definición, todos los valores de notificación serán únicos. No obstante, he elegido este método por razones de extensibilidad. Por ejemplo, supongamos que más adelante desea comenzar a usar notificaciones de rol con esta solución. No tendría sentido almacenar la notificación de rol "Empleado” o “Cliente”, o lo que sea, miles de veces. El selector de persona solo necesita saber que el valor existe para que lo haga disponible. Después de eso, quien lo tenga lo tendrá, solo debemos permitir que se use en el momento de conceder derechos en un sitio. Entonces, en función de eso, la clase que almacenará los valores de notificación únicos tendrá el aspecto siguiente:
namespace AzureClaimsData
{
public class UniqueClaimValue : TableServiceEntity
{
public string ClaimType { get; set; }
public string ClaimValue { get; set; }
public string DisplayName { get; set; }
public UniqueClaimValue() { }
public UniqueClaimValue(string ClaimType, string ClaimValue, string DisplayName)
{
this.PartitionKey = System.Web.HttpUtility.UrlEncode(ClaimType);
this.RowKey = ClaimValue;
this.ClaimType = ClaimType;
this.ClaimValue = ClaimValue;
this.DisplayName = DisplayName;
}
}
}
Hay un par de cosas que vale la pena mencionar aquí. En primer lugar, como en la clase anterior, el parámetro PartitionKey usa un valor UrlEncoded porque será el tipo de notificación, que contendrá las barras diagonales. en segundo lugar, tal como veo con frecuencia cuando se usa el almacenamiento de tablas de Azure, los datos se desnormalizan porque no existe un concepto de JOIN como lo hay en SQL. Técnicamente, puede realizar JOIN en LINQ, pero hay tantas cosas en LINQ que no se permiten usar cuando se trabaja con datos Azure (o estos tienen un muy mal rendimiento) que creo que es más fácil simplemente desnormalizarlo. Si tiene otras ideas, no dude en publicarlas en los comentarios; me encantaría saber lo que piensa. Entonces, en nuestro caso, el nombre para mostrar será “Email”, porque ese es el tipo de notificación que almacenaremos en esta clase.
Cola de notificaciones
La cola de notificaciones se explica por sí sola; almacenaremos solicitudes para “nuevos usuarios” en esa cola y luego un proceso de trabajo de Azure la leerá desde dicha cola y moverá los datos a la tabla de valores de notificación únicos. El principal motivo por el que se hace esto es que el trabajo con el almacenamiento de tablas de Azure a veces puede ser bastante latente, pero colocar un elemento en una cola es una tarea relativamente rápida. Al usar este método, se puede minimizar el impacto en nuestro sitio web de SharePoint.
Clases de acceso a datos
Uno de los aspectos más mundanos de trabajar con el almacenamiento de tablas de Azure y las colas es que siempre hay que escribir una clase de acceso a datos propia. Para el almacenamiento de tablas, se debe escribir una clase de contexto de datos y una clase de origen de datos. No dedicaré mucho tiempo en ello porque puede encontrar una enorme cantidad de información en Internet. Además, voy a adjuntar mi código de origen al proyecto de Azure a este artículo para que pueda consultarlo todo lo que quiera.
Hay un elemento importante que sí me gustaría señalar, que es simplemente una opción de estilo personal. A mí me gusta separar todo mi código de acceso a datos de Azure en un proyecto independiente. Así, puedo compilarlo en su propio ensamblado y usarlo incluso en proyectos que no sean de Azure. Por ejemplo, en el código de ejemplo que estoy cargando, encontrará una aplicación de formulario de Windows que usé para probar las distintas partes del back-end de Azure. No sabe nada de Azure, excepto que tiene una referencia a algunos ensamblados de Azure y a mi ensamblado de acceso a datos. Puedo usarlo en ese proyecto igual de fácil en mi proyecto WCF que uso para poner en el front-end el acceso a datos de SharePoint.
Sin embargo, aquí incluyo algunos elementos a tener en cuenta acerca de las clases de acceso a datos:
- · Tengo una clase “contenedor” separado para los datos que devolveré; los tipos de notificaciones y los valores de notificación únicos. Lo que quiero decir por clase contenedor es que tengo una clase simple con una propiedad pública de tipo List<>. Devuelvo esta clase cuando se soliciten los datos, en lugar de solo List<> de resultados. Esto es porque cuando devuelvo un valor List<> de Azure, el cliente solo obtiene el último elemento de la lista (cuando hace lo mismo desde un WCF hospedado localmente, funciona sin problemas). Entonces, para solucionar este problema, devuelvo tipos de notificación en una clase que tiene el aspecto siguiente:
namespace AzureClaimsData
{
public class ClaimTypeCollection
{
public List<ClaimType> ClaimTypes { get; set; }
public ClaimTypeCollection()
{
ClaimTypes = new List<ClaimType>();
}
}
}
Y la clase de devolución de valores de notificación únicos tiene el aspecto siguiente:
namespace AzureClaimsData
{
public class UniqueClaimValueCollection
{
public List<UniqueClaimValue> UniqueClaimValues { get; set; }
public UniqueClaimValueCollection()
{
UniqueClaimValues = new List<UniqueClaimValue>();
}
}
}
- · Las clases de contexto de datos son relativamente sencillas, no hay nada que destacar (como diría mi amiga Vesa). Tiene el aspecto siguiente:
namespace AzureClaimsData
{
public class ClaimTypeDataContext : TableServiceContext
{
public static string CLAIM_TYPES_TABLE = "ClaimTypes";
public ClaimTypeDataContext(string baseAddress, StorageCredentials credentials)
: base(baseAddress, credentials)
{ }
public IQueryable<ClaimType> ClaimTypes
{
get
{
//aquí es donde se configura el nombre de la tabla en el almacenamiento de tablas de Azure
//con el que trabajará
return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);
}
}
}
}
- · En las clases de origen de datos, sí uso un método ligeramente diferente a la hora de establecer la conexión con Azure. La mayoría de los ejemplos que veo en Internet quiere leer las credenciales con alguna clase de configuración de registro (no es el nombre exacto, pero no lo recuerdo). El problema con ese método aquí es que no tengo ningún contexto específico de Azure porque quiero que mi clase de datos funcione fuera de Azure. En cambio, simplemente creo una configuración en las propiedades de mi proyecto y en ella incluyo el nombre de usuario y la clave necesarios para establecer la conexión con mi cuenta de Azure. De este modo, mis dos clases de origen de datos tienen código que tienen el aspecto siguiente para establecer una conexión al almacenamiento de Azure:
private static CloudStorageAccount storageAccount;
private ClaimTypeDataContext context;
//constructor estático, para solo se active una vez
static ClaimTypesDataSource()
{
try
{
//obtener información de conexión de la cuenta de almacenamiento
string storeCon = Properties.Settings.Default.StorageAccount;
//extraer la información de la cuenta
string[] conProps = storeCon.Split(";".ToCharArray());
string accountName = conProps[1].Substring(conProps[1].IndexOf("=") + 1);
string accountKey = conProps[2].Substring(conProps[2].IndexOf("=") + 1);
storageAccount = new CloudStorageAccount(new StorageCredentialsAccountAndKey(accountName, accountKey), true);
}
catch (Exception ex)
{
Trace.WriteLine("Error initializing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
//nuevo constructor
public ClaimTypesDataSource()
{
try
{
this.context = new ClaimTypeDataContext(storageAccount.TableEndpoint.AbsoluteUri, storageAccount.Credentials);
this.context.RetryPolicy = RetryPolicies.Retry(3, TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
Trace.WriteLine("Error constructing ClaimTypesDataSource class: " + ex.Message);
throw;
}
}
- · La implementación real de las clases de origen de datos incluye un método para agregar un nuevo elemento tanto para un tipo de clase como para un valor de notificación única. Es código muy sencillo que tiene el aspecto siguiente:
//agregar un elemento nuevo
public bool AddClaimType(ClaimType newItem)
{
bool ret = true;
try
{
this.context.AddObject(ClaimTypeDataContext.CLAIM_TYPES_TABLE, newItem);
this.context.SaveChanges();
}
catch (Exception ex)
{
Trace.WriteLine("Error adding new claim type: " + ex.Message);
ret = false;
}
return ret;
}
Una diferencia importante que debe tenerse en cuenta en el método Add para el origen de datos de valores de notificación únicos es que no provoca un error ni devuelve "false" cuando se produce una excepción a la hora de guardar los cambios. Esto se debe a que espero que las personas intentarán, por error u otro motivo, registrarse varias veces. Una vez que disponga de un registro de su notificación de correo electrónico, todo intento subsiguiente para agregarla lanzará una excepción. Dado que Azure no nos da el lujo de poder usar excepciones fuertemente tipadas y dado que no quiero que el registro de seguimiento se llene con datos inútiles, no me preocuparé por ello cuando se produzca esta situación.
- · La búsqueda de notificaciones es algo más interesante, solo en la medida que expone nuevamente algunas de las cosas que puede hacer en LINQ, pero no en LINQ con Azure. Agregaré el código aquí y luego explicaré algunas de las elecciones que hice:
public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)
{
UniqueClaimValueCollection results = new UniqueClaimValueCollection();
UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();
const int CACHE_TTL = 10;
try
{
//busque el conjunto actual de valores de notificación en la memoria caché
if (HttpRuntime.Cache[ClaimType] != null)
results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];
else
{
//no está en la memoria caché, por lo que se debe consultar Azure
//Azure no admite starts with, por lo que todos los datos se deben obtener para el tipo de notificación
var values = from UniqueClaimValue cv in this.context.UniqueClaimValues
where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)
select cv;
//primero debe asignarlo para ejecutar realmente la consulta y devolver los resultados
results.UniqueClaimValues = values.ToList();
//almacenarlo en la memoria caché
HttpRuntime.Cache.Add(ClaimType, results, null,
DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,
System.Web.Caching.CacheItemPriority.Normal,
null);
}
//ahora realizar una consulta basada en los criterios para obtener la máxima cantidad de resultados
returnResults.UniqueClaimValues = (from UniqueClaimValue cv in results.UniqueClaimValues
where cv.ClaimValue.StartsWith(Criteria)
select cv).Take(MaxResults).ToList();
}
catch (Exception ex)
{
Trace.WriteLine("Error searching claim values: " + ex.Message);
}
return returnResults;
}
Lo primero que debe tenerse en cuenta es que no puede usar StartsWith con datos de Azure. Por tanto, eso significa que debe recuperar todos los datos localmente y luego usar la expresión StartsWith. Dado que recuperar todos esos datos sería una operación costosa (es como un examen de la tabla para recuperar todas las filas), lo hago una vez y almaceno los datos en caché. De ese modo, solamente tengo que realizar una recuperación “real” cada 10 minutos. La desventaja es que si se agregan usuarios durante ese tiempo, no podremos verlos en el selector de personas hasta que expire la memoria caché y se vuelvan a recuperar todos los datos. Asegúrese de recordar esto a la hora de consultar los resultados.
Una vez que tenga realmente mi conjunto de datos, puedo realizar StartsWith y también puedo limitar el número de registros que se devuelven. De manera predeterminada, SharePoint no mostrará más de 200 registros en el selector de personas, por lo que ese será el número máximo que pienso solicitar cuando se llame a este método. Sin embargo, lo incluyo como parámetro aquí para que pueda hacer lo que desee.
Clase de acceso a colas
Realmente, aquí no nada muy interesante. Simplemente hay algunos métodos básicos para agregar, leer y eliminar mensajes de la cola.
Rol de trabajo de Azure
El rol de trabajo también no tiene nada que destacar. Se activa cada 10 segundos y realiza una búsqueda para ver si hay nuevos mensajes en la cola. Para ello, llama a la clase de acceso a la cola. Si encuentra elementos en ella, divide el contenido (mediante punto y comas) en sus partes constituyentes, crea una nueva instancia de la clase UniqueClaimValue y luego intenta agregar esa instancia a la tabla de valores de notificación únicos. Una vez que haya hecho eso, elimina el mensaje de la cola y pasa al elemento siguiente hasta alcanzar el número máximo de mensajes que se pueden leer a la vez (32) o hasta que no queden más mensajes.
Aplicación WCF
Tal como describí anteriormente, la aplicación WCF es el elemento con el que el código de SharePoint se comunica para agregar elementos a la cola, obtener la lista de tipos de notificación y buscar o resolver n valor de notificación. Como una buena aplicación de confianza, tiene una relación de confianza establecida entre ella y la granja de servidores de SharePoint que la llama. Esto impide la suplantación de cualquier tipo de token a la hora de solicitar los datos. En este punto, no hay una seguridad más fina implementada en WCF en sí. Para conseguir una seguridad completa, WCF primero se probó en un servidor web local y luego avanzó hacia Azure donde se volvió a probar para confirmar que todo funciona correctamente.
Entonces, esos son los elementos básicos de los componentes de Azure de esta solución. Con suerte, esta información de fondo explica qué son todas las partes movibles y cómo se usan. En la parte siguiente, hablaré sobre el proveedor de notificaciones personalizado de SharePoint y cómo vincularemos todas estas piezas para nuestra solución de extranet “llave en mano”. Los archivos adjuntos a este artículo contienen todo el código de origen para la clase de acceso a datos, el proyecto de prueba, el proyecto Azure, el rol de trabajo y los proyectos WCF. También contiene una copia de este artículo en un documento Word, para que pueda consultar mi intención para este contenido antes de que la presentación en este sitio lo haya machacado.
Esta entrada de blog es una traducción. Puede consultar el artículo original en The Azure Custom Claim Provider for SharePoint Project Part 2