O Provedor de Declarações Personalizado Azure para o Projeto SharePoint Parte 2
Artigo original publicado quarta-feira, 15 de fevereiro de 2012
Na Parte 1 desta série, eu esbocei resumidamente a meta deste projeto, que, em um alto nível, é usar o armazenamento de tabelas do Windows Azure como armazenamento de dados para um provedor de declarações personalizado do SharePoint. O provedor de declarações deverá usar o Kit CASI para recuperar os dados necessários do Windows Azure para fornecer a funcionalidade de selecionador de pessoas (ou seja, catálogo de endereços) e de resolução de nome de controle de tipo.
Na Parte 3, criei todos os componentes usados no farm do SharePoint. Isso inclui um componente personalizado baseado no Kit CASI que gerencia toda a comunicação entre o SharePoint e o Azure. Há uma Web Part personalizada que captura informações sobre novos usuários e as coloca em uma fila do Azure. Finalmente, há um provedor de declarações personalizado que se comunica com o armazenamento de tabelas do Azure por meio de um WCF - através do componente personalizado do Kit CASI - para ativar a funcionalidade de controle de tipo e selecionador de pessoas.
Agora, vamos expandir um pouco mais esse cenário.
Esse tipo de solução se conecta muito bem a um cenário muito comum, que é quando você deseja uma extranet minimamente gerenciada. Você deseja, por exemplo, que seus parceiros ou clientes consigam visitar um site seu da Web, solicitar uma conta e, então, consigam automaticamente “fornecer” essa conta…onde “fornecer” pode significar muitas coisas diferentes para pessoas diferentes. Vamos usar esse como cenário de linha de base aqui, mas, naturalmente, vamos deixar nossos recursos de nuvem pública fazer uma parte do trabalho para nós.
Vamos começar olhando os componentes em nuvem para nosso próprio desenvolvimento:
- Uma tabela para controlar todos os tipos de declarações às quais ofereceremos suporte
- Uma tabela para controlar todos os valores de declarações exclusivas para o selecionador de pessoas
- Uma fila em que podemos enviar dados que devem ser adicionados à lista de valores de declarações exclusivas
- Algumas classes de acesso de dados para ler e gravar dados de tabelas do Azure e gravar dados na fila
- Uma função de funcionário do Azure que vai ler dados fora da fila e preencher a tabela de valores de declarações exclusivas
- Um aplicativo WCF que será o ponto de extremidade através do qual o farm do SharePoint se comunica para obter a lista de tipos de declarações, procurar declarações, resolver uma declaração e adicionar dados à fila
Agora, vamos ver cada um com mais detalhes.
Tabela de Tipos de Declarações
A tabela de tipos de declarações é onde vamos armazenar todos os tipos de declarações que nosso provedor de declarações personalizado pode usar. Neste cenário, vamos usar apenas um tipo de declaração, que é a declaração de identidade – que, neste caso, será o endereço de email. É possível usar outras declarações, mas para simplificar este cenário, vamos usar apenas essa. No armazenamento de tabelas do Azure, você adiciona instâncias de classes a uma tabela, portanto, precisamos criar uma classe para descrever os tipos de declarações. Novamente, observe que você pode ter instâncias de tipos de classe diferentes na mesma tabela do Azure, mas para que as coisas continuem funcionando, não vamos fazer isso aqui. A classe que esta tabela usará é semelhante ao seguinte:
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;
}
}
}
Não vou abranger todas as noções básicas do trabalho com o armazenamento de tabelas do Azure porque há muitos recursos lá fora que já fizeram isso. Portanto, se você desejar mais detalhes sobre o que é um PartitionKey ou RowKey e como usá-los, o mecanismo de busca Bing local acessível poderá ajudá-lo. A única coisa que vale a pena apontar aqui é que estou codificando em URL o valor que estou armazenando para o PartitionKey. Por quê? Bem, neste caso, meu PartitionKey é o tipo de declaração, que pode ter vários formatos: urn:foo:blah, https://www.foo.com/blah, etc. No caso de um tipo de declaração que inclui barras, o Azure não pode armazenar o PartitionKey com esses valores. Então, nós os codificamos em um formato acessível que agrade ao Azure. Como afirmei acima, em nosso caso estamos usando a declaração de email de forma que o tipo de declaração para ela é https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
Tabela de Valores de Declarações Exclusivas
A tabela Valores de Declarações Exclusivas é onde todos os valores de declarações exclusivas obtidos são armazenados. Em nosso caso, estamos armazenando apenas um tipo de declaração – a declaração de identidade – então, por definição todos os valores de declarações devem ser exclusivos. No entanto, fiz essa abordagem por razões de extensibilidade. Por exemplo, suponha que você queira começar a usar as declarações de Função com esta solução. Não faria sentido armazenar a declaração de Função “Funcionário” ou “Cliente” ou milhares de outras diferentes; para o selecionador de pessoas, é necessário apenas saber se o valor existe para que seja disponibilizado no selecionador. Depois disso, quem quer que o tenha, tem – precisamos apenas deixar que ele seja usado ao conceder direitos em um site. Portanto, com base nisso, a seguir é mostrado como será a aparência da classe que armazenará os valores de declarações exclusivas:
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;
}
}
}
Há algumas coisas que vale a pena apontar aqui. Primeiro, como a classe anterior, o PartitionKey usa um valor UrlEncoded porque ele será o tipo de declaração, que terá nele as barras. Segundo, como vejo frequentemente ao usar o armazenamento de tabela do Azure, os dados são desordenados porque não há um conceito JOIN como há no SQL. Tecnicamente, é possível fazer um JOIN em LINQ, mas muitas coisas que estão no LINQ foram desativadas ao trabalhar com dados do Azure (ou executar de forma incorreta) que eu achei mais fácil apenas desordenar. Se vocês tiverem outras ideias sobre isso escreva-as nos comentários – eu gostaria de saber o que vocês pensam. Portanto, em nosso caso, o nome para exibição será “Email” porque esse é o tipo de declaração que estamos armazenando nessa classe.
A Fila de Declarações
A fila de declarações é muito simples – nós vamos armazenar solicitações para “novos usuários” nessa fila e depois um processo de funcionário do Azure irá ler isso na fila e mover os dados para a tabela de valores de declarações exclusivas. A principal razão para fazer isso é que trabalhar com o armazenamento de tabelas do Azure pode, algumas vezes, ser muito latente, mas colocar um item em uma fila é muito rápido. Seguir essa abordagem significa que podemos minimizar o impacto em nosso site da Web do SharePoint.
Classes de Acesso a Dados
Um dos aspectos mais básicos em se trabalhar com o armazenamento de tabelas do Azure e filas é que você sempre tem que gravar sua própria classe de acesso a dados. Para o armazenamento de tabelas, é necessário gravar uma classe de contexto de dados e uma classe de fonte de dados. Não vou gastar muito tempo com isso porque você pode ler muita coisa sobre isso na Web; além disso, estou também anexando meu código-fonte para o projeto Azure nessa postagem para que você possa ver tudo o que desejar.
Há uma coisa importante que eu gostaria de apontar aqui, que é apenas uma opção de estilo pessoal. Gosto de quebrar todo o meu código de acesso a dados do Azure em um projeto separado. Assim posso compilá-lo em seu próprio assembly e posso usá-lo também a partir de projetos não-Azure. Por exemplo, no código de amostra que estou carregando você localizará um aplicativo de formulário do Windows que usei para testar as diferentes partes do back-end do Azure. Ele não sabe nada sobre o Azure, ele possui uma referência a alguns assemblies do Azure e ao meu assembly de acesso a dados. Eu posso usá-lo nesse projeto e muito facilmente no meu projeto WCF que uso para front-end do acesso a dados para o SharePoint.
Aqui estão algumas das particularidades sobre as classes de acesso a dados:
- · Tenho uma classe de “contêiner” separada para os dados que vou retornar – os tipos de declarações e os valores de declarações exclusivas. O que quero dizer com classe de contêiner é que tenho uma classe simples com uma propriedade pública de Lista de tipo<>. Eu retorno essa classe quando os dados são solicitados, em vez de apenas uma Lista<> de resultados. A razão pela qual faço isso é porque quando retorno uma Lista<> do Azure, o cliente obtém apenas o último item na lista (quando a mesma coisa é feita de um WCF hospedado localmente, tudo funciona bem). Então, para resolver esse problema eu retorno os tipos de declarações em uma classe semelhante ao seguinte:
namespace AzureClaimsData
{
public class ClaimTypeCollection
{
public List<ClaimType> ClaimTypes { get; set; }
public ClaimTypeCollection()
{
ClaimTypes = new List<ClaimType>();
}
}
}
E a classe de retorno de valores de declarações exclusivas é semelhante ao seguinte:
namespace AzureClaimsData
{
public class UniqueClaimValueCollection
{
public List<UniqueClaimValue> UniqueClaimValues { get; set; }
public UniqueClaimValueCollection()
{
UniqueClaimValues = new List<UniqueClaimValue>();
}
}
}
- · As classes de contexto de dados são muito simples – nada realmente brilhante aqui (como diria meu amigo Vesa); é semelhante ao seguinte:
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
{
//this is where you configure the name of the table in Azure Table Storage
//that you are going to be working with
return this.CreateQuery<ClaimType>(CLAIM_TYPES_TABLE);
}
}
}
}
- · Nas classes de fonte de dados eu realmente tenho uma abordagem um pouco diferente para fazer a conexão com o Azure. A maioria dos exemplos que vejo na Web quer ler as credenciais com alguma classe de configurações de reg (não é esse o nome exato, não me lembro qual é). O problema com essa abordagem aqui é que não tenho um contexto específico do Azure porque quero que minha classe de dados funcione fora do Azure. Então, em vez disso, eu simplesmente crio uma Configuração nas minhas propriedades do projeto e ali incluo o nome da conta e a chave necessária para conectar à minha conta do Azure. Assim, minhas duas classes de fonte de dados têm código semelhante ao seguinte para criar essa conexão com o armazenamento do Azure:
private static CloudStorageAccount storageAccount;
private ClaimTypeDataContext context;
//static constructor so it only fires once
static ClaimTypesDataSource()
{
try
{
//get storage account connection info
string storeCon = Properties.Settings.Default.StorageAccount;
//extract account info
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;
}
}
//new 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;
}
}
- · A implementação real das classes de fonte de dados inclui um método para adicionar um novo item para um tipo de declaração e também para um valor de declaração exclusiva. É um código muito simples semelhante ao seguinte:
//add a new item
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;
}
Uma diferença importante a ser observada no método Adicionar para a fonte de dados de valores de declarações exclusivas é que ele não lança um erro ou retorna falso quando há uma exceção em salvar as alterações. Isso porque espero realmente que as pessoas erroneamente ou de outra forma experimentem e entrem várias vezes. Depois que tivermos um registro da declaração de email, qualquer tentativa subsequente de adicioná-lo lançará uma exceção. Como o Azure não nos fornece o luxo de exceções fortemente digitadas e como eu não quero que o log de rastreio esteja cheio de uma sentimentalidade exagerada sem sentido, não me preocupo com isso quando ocorre essa situação.
- · Procurar declarações é um pouco mais interessante, apenas no que se refere ao fato de que isso expõe novamente algumas coisas que podem ser feitas no LINQ, mas não no LINQ com o Azure. Vou incluir o código aqui e explicar algumas escolhas que fiz:
public UniqueClaimValueCollection SearchClaimValues(string ClaimType, string Criteria, int MaxResults)
{
UniqueClaimValueCollection results = new UniqueClaimValueCollection();
UniqueClaimValueCollection returnResults = new UniqueClaimValueCollection();
const int CACHE_TTL = 10;
try
{
//look for the current set of claim values in cache
if (HttpRuntime.Cache[ClaimType] != null)
results = (UniqueClaimValueCollection)HttpRuntime.Cache[ClaimType];
else
{
//not in cache so query Azure
//Azure doesn't support starts with, so pull all the data for the claim type
var values = from UniqueClaimValue cv in this.context.UniqueClaimValues
where cv.PartitionKey == System.Web.HttpUtility.UrlEncode(ClaimType)
select cv;
//you have to assign it first to actually execute the query and return the results
results.UniqueClaimValues = values.ToList();
//store it in cache
HttpRuntime.Cache.Add(ClaimType, results, null,
DateTime.Now.AddHours(CACHE_TTL), TimeSpan.Zero,
System.Web.Caching.CacheItemPriority.Normal,
null);
}
//now query based on criteria, for the max results
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;
}
A primeira coisa a observar é que não é possível usar o StartsWith junto com dados do Azure. Isso significa que é necessário recuperar todos os dados localmente e usar a expressão StartsWith. Como recuperar todos esses dados pode ser uma operação cara (é efetivamente uma verificação da tabela para recuperar todas as linhas), eu faço isso uma vez e depois armazeno os dados em cache. Assim, tenho apenas que fazer uma rechamada “real” a cada 10 minutos. A desvantagem é que se forem adicionados usuários durante esse tempo, nós não conseguiremos vê-los no selecionador de pessoas até o cache expirar e nós recuperarmos todos os dados novamente. Lembre-se disso quando estiver buscando os resultados.
Depois que eu realmente tiver meu conjunto de dados, poderei fazer o StartsWith e também poderei limitar a quantidade de registros que retorno. Por padrão, o SharePoint não exibirá mais de 200 registros no selecionador de pessoas, então, essa será a quantidade máxima que planejo solicitar quando este método é chamado. Mas eu o estou incluindo como um parâmetro aqui para que você possa fazer o que desejar.
A Classe de Acesso à Fila
Honestamente, não há nada super interessante aqui. Apenas alguns métodos básicos para adicionar, ler e excluir mensagens da fila.
Função de Funcionário do Azure
A função de funcionário também não é muito descrita. Ele levanta a cada 10 segundos e vê se há alguma mensagem nova na fila. Ele faz isso chamando a classe de acesso à fila. Se localizar algum item lá, divide o conteúdo (que é delimitado por ponto-e-vírgula) em suas partes constituintes, cria uma nova instância da classe UniqueClaimValue e tenta adicionar essa instância à tabela de valores de declarações exclusivas. Depois de fazer isso, exclui a mensagem da fila e passa para o próximo item, até atingir o número máximo de mensagens que podem ser lidas de uma vez (32) ou até que não reste nenhuma mensagem.
Aplicativo WCF
Conforme descrito anteriormente, o aplicativo WCF é aquele com o qual o código SharePoint se comunica para adicionar itens à fila, obter a lista de tipos de declarações e procurar ou resolver um valor de declaração. Como um bom aplicativo de confiança, ele tem uma confiança estabelecida entre ele e o farm do SharePoint que o está chamando. Isso evita qualquer tipo de falsificação de token ao solicitar os dados. Nesse ponto, não há nenhuma segurança mais refinada implementada no WCF em si. Para completar, o WCF foi testado primeiro em um servidor Web local e depois movido para o Azure, onde foi testado novamente para confirmar se tudo estava funcionando.
Essas são as noções básicas dos componentes do Azure dessa solução. Felizmente, esse plano de fundo explica quais são todas as partes móveis e como elas são usadas. Na próxima parte, discutirei o provedor de declarações personalizado do SharePoint e como juntamos todas essas partes para nossa solução de extranet “turnkey”. Os arquivos anexados nesta postagem contêm todo o código-fonte para a classe de acesso a dados, o projeto de teste, o projeto do Azure, a função de funcionário e os projetos do WCF. Contém também uma cópia dessa postagem em um documento do Word; sendo assim, você pode realmente compreender minha intenção para este conteúdo antes da renderização neste site acabar com ela.
Esta é uma postagem em blog localizado. Localize o artigo original em The Azure Custom Claim Provider for SharePoint Project Part 2