Olá pessoal, tudo certo?
Dando continuidade a série sobre REST, mostro como implementar um serviço REST, baseado no protocolo OData, utilizando WCF Data Services. Se você não entende de REST, recomendo fortemente os posts anteriores.
Buscando fugir do exemplo simples, não crio um WCF Data Services consumindo um contexto do Entity Framework. No lugar disso, crio um contexto com objetos em memória e implemento suporte completo a atualização (com IUpdatable)
Vamos aos fatos.
Algumas palavras sobre OData
OData é um protocolo extremamente poderoso, desenvolvido pela Microsoft, para construção de serviços com características REST (Há quem defenda que OData não é RESTful). Segue definição presente em OData.org:
The Open Data Protocol (OData) is a Web protocol for querying and updating data that provides a way to unlock your data and free it from silos that exist in applications today. OData does this by applying and building upon Web technologies such as HTTP, Atom Publishing Protocol(AtomPub) and JSON to provide access to information from a variety of applications, services, and stores. The protocol emerged from experiences implementing AtomPub clients and servers in a variety of products over the past several years. OData is being used to expose and access information from a variety of sources including, but not limited to, relational databases, file systems, content management systems and traditional Web sites.
Se você nunca usou OData, considere dar uma olhada na página de exemplos práticos, disponível em http://www.odata.org/developers/protocols/uri-conventions.
Principais elementos arquitetônicos envolvidos com aplicações OData
Se está considerando o desenvolvimento de serviços OData, considere os seguintes elementos:
- Model – Classes representando a unidade dos recursos que desejamos expor no serviço. Um aspecto importante é que toda instância deverá ser identificável por uma chave única (um Id).
- Context – Um agrupamento de recursos estabelecendo seus relacionamentos. O contexto é o responsável por “materializar” e garantir a persistência das entidades (instâncias de objeto) com tipos definidos no modelo.
- Service – Implementa a publicação dos dados disponíveis no Context definindo o suporte para diversos formatos. Além disso, define e valida as interações dos agentes externos com o Context. É nele que devemos implementar regras de segurança.
- Host – Responsável pela publicação do serviço. É o elemento endereçavel que “segura” o serviço em execução. É quem define o tempo de vida do serviço.
Implementação OData mais “frequente”
Quando vemos alguém implementar serviços OData, geralmente vemos:
- Entity Framework sendo usado para definir o modelo de objetos (Model) e contexto de acesso a dados (Context);
- WCF Data Services implementando um serviço com a lógica OData;
- IIS para plublicação e HOST do serviço.
Implementação OData concreta que faremos hoje
Buscando mostrar a “competência” da plataforma, seguiremos caminho diferente:
- Implementaremos modelo de objetos (model) utilizando classes “simples” (quase POCO);
- Implementaremos um Context POCO
- Implementaremos o serviço usando WCF Data Services (porque ele é muito bom)
- Publicaremos o serviço usando WcfServiceHost em uma Console Application.
Implementando o modelo
Nossa implementação é muito simples. Observe:
[DataServiceKey("Id")] public class Person { public int Id { get; set; } public string Name { get; set; } public Enterprise EmployerField = null; public Enterprise Employer { get { return this.EmployerField; } set { if (value == this.EmployerField) return; if (this.EmployerField != null && this.EmployerField.Employees.Contains(this)) this.EmployerField.Employees.Remove(this); this.EmployerField = value; if (this.EmployerField != null && !this.EmployerField.Employees.Contains(this)) this.EmployerField.Employees.Add(this); } } } [DataServiceKey("Id")] public class Enterprise { public Enterprise() { this.Employees.CollectionChanged += (sender, e) => { if (e.OldItems != null) foreach (Person item in e.OldItems) item.Employer = null; if (e.NewItems != null) foreach (Person item in e.NewItems) item.Employer = this; }; } public int Id { get; set; } public string Name { get; set; } ObservableCollection_employees = new ObservableCollection (); public ObservableCollection Employees { get { return _employees; } } }
Basicamente, definimos duas classes: Person e Enterprise. Sendo que:
- Ambas definem duas propriedades simples (Id e Name)
- Em ambas, utilizo o atributo DataServiceKey sinalizando o nome da propriedade correspondente a chave única. Essa é uma exigência do Reflection Provider , que será utilizado pelo serviço que construiremos utilizando WCF Data Services.
- Na classe Person, criei uma propriedade Link (terminologia OData para Navigation Properties do Entity Framework) chamada Employer (Empresa onde essa “pessoa” trabalha).
- Na classe Enterprise, criei uma propriedade Link chamada Employees que contém uma coleção com referências de Person correspondentes aos funcionários da empresa.
Observe que garanto, nos tipos do modelo, a consistência relacional. Para isso:
- Modifiquei o setter da propriedade Employer, na classe Person, para que esse inclua ou remova uma referência da instância na Enterprise relacionada.
- Observo as modificações da coleção Employees de forma a atualizar as instâncias de Person afetadas.
Implementação de um Context read-only para Reflection Provider (WCF Data Services)
Como já foi dito, o contexto é responsável por prover o conjunto de recursos, cuidando da materialização e persistência destes. Geralmente, utiliza-se Entity Framework para prover esse contexto. Hoje, implementaremos um contexto mais simples, que opera com objetos em memória, mas que pode ser adaptado para materializar e persistir dados em fontes diversas, como, por exemplo, FileSystem ou NHibernate.
Esse primeiro contexto que apresento opera em modo “read-only”. Ou seja, sem suporte para modificações. Observe:
public class ReadOnlyDataContext { static Person[] _people; static Enterprise[] _enterprises; static ReadOnlyDataContext() { _people = new [] { new Person() { Id = 0, Name="Elemar" }, new Person() { Id = 1, Name="Felipe" } }; _enterprises = new [] { new Enterprise() { Id = 0, Name="Procad" } }; _enterprises[0].Employees.Add(_people[0]); } public IQueryablePeople { get { return _people.AsQueryable (); } } public IQueryable Enterprises { get { return _enterprises.AsQueryable (); } } }
Essa classe pode ser dividida em dois blocos:
- carga de dados providos pelo contexto – usando, nesse exemplo, dados constantes carregados no construtor estático. Poderia “ousar” algum dinamismo, consultando um artefato de persistência toda vez que um determinado tipo de dado fosse solicitado.
- fornecimento de dados – todos, obrigatoriamente, coleções que implementem IQueryable, em interfaces públicas.
Implementando o Serviço WCF usando WCF Data Services
A implementação do serviço usando WCF Data Services é fácil. Observe:
public class ODataService : DataService< ReadOnlyDataContext > { public static void InitializeService(IDataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.All); config.UseVerboseErrors = true; } }
Perceba que, obviamente, essa é uma versão muito resumida de um serviço OData, implementado usando WCF Data Services. Tudo que precisei fazer aqui foi definir, via parâmetro de tipo para a classe, o Context associado.
O método estático InitializeService é chamado pela classe base (DataService
Implementado o Host
Já temos nosso Modelo, Contexto e Serviço. Agora, resta apenas implementar o host. Seguindo a tradição (veja todos os meus posts com serviços WCF), fiz isso implementando usando a fantástica WcfServiceHost. Observe:
class Program { static void Main(string[] args) { var wsh = new WebServiceHost(typeof(ODataService), new Uri("http://localhost:666")); wsh.Open(); Console.WriteLine("Service is live. Press ENTER to close."); Console.ReadLine(); Console.WriteLine("Closing ..."); wsh.Close(); Console.WriteLine("Closed!"); } }
Perfeito!
Observe que já conseguimos executar todas as operações de consulta (verbot HTTP GET), com esse serviço.
Para http://localhost:666, temos:
Listando People, com http://localhost:666/People, temos:
Requisitando os metadados, com http://localhost:666/$metadata, temos:
Bacana, não!?
Lembre-se de explorar todas as possibilidades de consulta. Consulte a referência disponível em http://www.odata.org/developers/protocols/uri-conventions.
Indo além do “somente-leitura”
Prover mais do que Read-Write depende da implementação, no contexto, da interface IUpdatable. Ela descreve um contrato bem simples para realizar as alterações.
public interface IUpdatable { void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded); void ClearChanges(); object CreateResource(string containerName, string fullTypeName); void DeleteResource(object targetResource); object GetResource(IQueryable query, string fullTypeName); object GetValue(object targetResource, string propertyName); void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved); object ResetResource(object resource); object ResolveResource(object resource); void SaveChanges(); void SetReference(object targetResource, string propertyName, object propertyValue); void SetValue(object targetResource, string propertyName, object propertyValue); }
Recomendo que você consulte a documentação no MSDN para entender o propósito de cada método dessa interface. Além disso, recomendo a leitura da página sobre operações do OData
Adaptando o contexto para que ele suporte alterações
Nosso primeiro contexto armazenava dados em vetores. Vamos modificar essa classe para que ela aceite alterações no modelo de dados. Observe:
public class DataContext : IUpdatable { static List< Person > _people; static List< Enterprise > _enterprises; static DataContext() { _people = new List< Person > { new Person() { Id = 0, Name="Elemar" }, new Person() { Id = 1, Name="Felipe" } }; _enterprises = new List< Enterprise > { new Enterprise() { Id = 0, Name="Procad" } }; _enterprises[0].Employees.Add(_people[0]); } public IQueryable< Person > People { get { return _people.AsQueryable< Person >(); } } public IQueryable< Enterprise > Enterprises { get { return _enterprises.AsQueryable< Enterprise >(); } } //... }
Alterar os vetores por listas torna possível incluir e remover registros.
Acumulando mudanças: Métodos SaveChanges e ClearChanges
WCF Data Services permite que diversas alterações sejam “planejadas” antes de serem efetivamente implantadas nos dados. Para entregar essa funcionalidade, penso em acumular todas as modificações realizadas em Actions executando-as todas, em sequência, quando o método SaveChanges for evocado pela infraestrautura do WCF Data Services. Observe:
static List_changes = new List (); public void ClearChanges() { _changes.Clear(); } public void SaveChanges() { foreach (var change in _changes) change(); _changes.Clear(); }
Acionando verbos HTTTP PUT, POST e DELETE através do Fiddler
Em REST, executar alterações implica em requests com verbos HTTP não acessíveis em um browser. Para isso, uso o Fiddler.
Se você ainda não sabe usar o Fiddler, gasta um tempo para aprender. Resumindo, na aba Request Builder encontramos nossa principal ferramenta de trabalho. Nela, podemos informar o endereço, o verbo http, o cabeçalho e o corpo da mensagem.
Composta a mensagem, basta clicar no botão Execute. O resultado poderá ser conferido na lista Web Sessions.
Implementando a exclusão de um recurso
Como você já viu na página de referência, para excluir um recurso OData, executamos um HTTP DELETE “contra” o endereço do recurso que desejamos remover.
Como exemplo, no Fiddler, selecionamos o verbo DELETE, informamos o endereço http://localhost:666/People(0) e clicamos em Execute. A infraestrutura do WCF Data Services começa os trabalhos executando o método GetResourse. Para nosso exemplo, eis a implementação que desenvolvi:
public object GetResource(IQueryable query, string fullTypeName) { object resource = query.Cast< object >().SingleOrDefault(); return resource; }
Perceba que o primeiro parâmetro é uma consulta “LINQ” construída pela infraestrutura WCF Data Services a partir do endereço fornecido no Request. Se você estiver implementando um Context que atua sobre um source de dados como que não suporte LINQ, precisará interpretar essa Query para recuperar dados adequadamente.
Após recuperar o Resource, WCF Data Services chamará o método ResolveResource. Este método tem por responsabilidade recuperar um recurso de trabalho a partir de um recurso de referência. Trata-se de um artifício poderoso, entretanto, desnecessário para o exemplo que estamos trabalhando. Por agora, basta retornar o próprio objeto recebido como argumento. Observe:
public object ResolveResource(object resource) { return resource; }
Finalmente, WCF Data Services chamará o método DeleteResource. Nesse método, recebo por parâmetro o objeto a remover. No corpo do método, verifico o tipo do recurso e, partindo disso, decido que coleção devo processar. Observe:
public void DeleteResource(object targetResource) { if (targetResource == null) return; if (targetResource.GetType() == typeof(Enterprise)) _changes.Add(() => _enterprises.Remove((Enterprise)targetResource)); else if (targetResource.GetType() == typeof(Person)) _changes.Add(() => _people.Remove((Person)targetResource)); }
Perceba que não processo a operação diretamente. No lugar disso, adiciono um Action na coleção _changes. Essa ação é finalmente executada pelo WCF Data Services, no próximo passo, pela evocação do método SaveChanges (que já apresentei).
Implementando a alteração de um recurso
Já conseguimos excluir um recurso. Agora precisamos saber como alterar esse recurso. No Fiddler, há três cuidados principais que precisam ser tomados:
- Precisamos setar o verbo para PUT e o endereço correspondente ao recurso que desejamos alterar;
- Precisamos informar, no header da request, o formato dos dados que estamos enviando no body da mensagem. No nosso exemplo, precisamos informar “Content-Type: application/atom+xml”
- Informamos, no body, os dados correspondentes ao corpo da entidade que desejamos alterar (recomendo que você execute um GET sobre o recurso para obter esse dado)
Por exemplo, vamos alterar o nome da pessoa com Id 0 de “Elemar” para “Elemar JR”. Segue o XML correspondente ao recurso modificado:
xmlns:d=http://schemas.microsoft.com/ado/2007/08/dataservices
xmlns:m=http://schemas.microsoft.com/ado/2007/08/dataservices/metadata
xmlns="http://www.w3.org/2005/Atom">
http://schemas.microsoft.com/ado/2007/08/dataservices/related/Employer
type="application/atom+xml;type=entry" title="Employer" href="People(0)/Employer" />
scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
WCF Data Services começa a alteração evocando o método GetResource (já implementado), seguido pelo método ResolveResource (também já implementado) e ResetResource. A idéia desse método é colocar todas as propriedades do objeto em seu valor default. Por hora, retorno o próprio objeto sem alterações.
public object ResetResource(object resource) { return resource; }
Com o objeto “resetado”, WCF Data Services determina o valor para cada propriedade usando o método SetValue. Como estamos trabalhando com objetos em memória, uso reflection para atualizar o valor das diversas propriedades. Observe:
public void SetValue(object targetResource, string propertyName, object propertyValue) { var property = targetResource.GetType().GetProperty(propertyName); _changes.Add(() => property.SetValue(targetResource, propertyValue, null)); }
Perceba que vou acumulando as alterações na coleção de _changes. Todas as alterações são efetivadas em seguida, pela chamada do método SaveChanges.
Implementando a alteração de uma propriedade de um recurso
Você percebeu o quando é trabalhoso alterar uma entidade. Por padrão, precisamos fornecer sempre todos os dados da entidade, mesmo que para alterar apenas uma propriedade. Entretanto, há uma alternativa: assim como podemos recuperar apenas o valor de uma propriedade, podemos modificar o valor de uma propriedade também. Para recuperar o valor de uma propriedade, informamos o endereço do recurso com o sufixo
Para alterar o valor de apenas uma propriedade, no Fiddler, executamos as seguintes etapas:
- Na aba Request Builder, mudamos o verbo http para PUT;
- informamos o endereço da propriedade que desejamos alterar;
- incluímos o header “Content-Type: text/plain”;
- Informamos o novo valor no campo body;
- Executamos o método Execute;
WCF Data Services evocará os métodos GetResource, ResolveResource, SetValue, SaveChanges e, novamente, ResolveResource para efetivar a alteração.
Simples demais.
Incluindo uma nova entidade
Incluir uma nova entidade é um processo bem semelhante a alterar uma existente. A dinâmica, no Fiddler, consiste em:
- Na aba Request Builder, selecionar o verbo POST e informar o endereço do conjunto correspondente ao recurso (por exemplo: http://localhost:666/People)
- No header, informar o formato do conteúdo no body. (Em nossos exemplos, application/atom+xml);
- No body, informar o XML correspondente (mesmo formato que informamos no exemplo do Update) e Execute!
O WCF Data Services começa os trabalhos evocando o método CreateResource. Esse método tem a respondabilidade de instanciar um objeto correspondente ao recurso que estamos incluindo. Observe:
public object CreateResource(string containerName, string fullTypeName) { object result = null; if (containerName == "People") { var person = new Person(); result = person; _changes.Add(() => AddPerson(person)); } else if (containerName == "Enterprises") { var enterprise = new Enterprise(); result = enterprise; _changes.Add(() => AddEnterprise(enterprise)); } return result; }
Repare como retardo a adição do objeto na coleção correspondente através da coleção _changes. Observe também que adiciono uma chamada a um método especial de adição. Faço isso para controlar o incremoendo do ID.
public void AddPerson(Person person) { var id = _people.Max((p) => p.Id) + 1; person.Id = id; _people.Add(person); } public void AddEnterprise(Enterprise enterprise) { var id = _enterprises.Max((p) => p.Id) + 1; enterprise.Id = id; _enterprises.Add(enterprise); }
Com um pouco de criatividade, dá para tornar a implementação desses métodos mais genérica. Por hora, está good enough.
WCF Data Services, segue a “materialização” da entidade por chamadas sucessivas do método SetValue. Persistência com SaveChanges.
Estabelecendo link entre uma coleção (propriedade Employees de Enterprise) e uma entidade (Person)
Se você der uma olhada na inicialização de nosso contexto, perceberá que, das duas pessoas criadas, há apenas um empregado na empresa Procad. Podemos confirmar isso com uma consulta simples e com uma resposta um tanto verbosa:
Ou ainda, podemos usar um artifício ainda mais simples e ter um resultado mais “econômico”.
É este o link que usaremos para adicionar referências para mais objetos. Eis a dinâmica:
- Na aba Request Builder, modificar verbo para POST e informar endereço no formato do segundo exemplo;
- Adicionar, no header, “Content-Type: application/xml”;
- No body, informar o link correspondente ao recurso que estamos adicionando (ex: http://localhost:666/People(1)) e Execute!
WCF Data Services evoca GetResource para recuperar o recurso que estamos adicionando. Depois, evoca o método AddReferenceToCollection, para relacionar o objeto recuperado a coleção correspondente. Observe a implementação que fiz para esse exemplo:
public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded) { _changes.Add(() => ((Enterprise)targetResource).Employees.Add((Person)resourceToBeAdded)); }
Note que, como estou trablhando com um modelo simplificado, selecionei facilmente a coleção que está sendo alterada. Em cenários mais complexos, preciso usar algum reflection para completar o processo.
Por fim, conclui a alteração evocando SaveChanges.
Removendo um link entre uma coleção (propriedade Employees de Enterprise) e uma entidade (Person)
Remover link é mais fácil que incluir. Basta executar um “DELETE” no endereço do link.
A dinâmica, no Fiddler, é muito simples:
- Na aba Request Builder, selecione o verbo DELETE e informe o endereço do link (mesmo formato da figura acima)
- Execute!
WCF Data Services evoca o método GetResource duas vezes (para obter a entidade que deverá ser removida da coleção e para obter a entidade com a coleção a ser manipulada). Por fim, evoca o método RemoveReferenceFromCollection para operar a modificação e SaveChanges para consolidar o resultado. Eis a implementação para RemoveReferenceFromCollection:
public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved) { _changes.Add(() => ((Enterprise)targetResource) .Employees.Remove((Person)resourceToBeRemoved)); }
Manipulando links entre propriedades simples (propriedade Employer de Person) e uma entidade (Enterprise)
A manutenção de links entre propriedades simples e entidades é bem semelhante a que executamos nas duas seções anteriores. Entretanto, com apenas um método. Eis suas implementações:
public void SetReference(object targetResource, string propertyName, object propertyValue) { _changes.Add(() => ((Person)targetResource).Employer = (Enterprise)propertyValue); }
Há apenas um método porque quando é executado um “DELETE”, o valor passado em propertyValue é null.
Por hoje, era isso!
março 30th, 2011 → 3:28 am
[...] usar como exemplo o código demonstrado no post anterior. A missão de hoje será poder vincular uma foto para cada “Person” provida pelo serviço (só [...]