.NET

16 ago, 2018

GraphQL – Construindo uma API GraphQL em .NET

Publicidade

Fala, galera!

GraphQL é uma tecnologia que basicamente foi desenvolvida pelo Facebook para o consumo de sua API. Ele é uma alternativa ao OData (Open Data Protocol) e sua principal missão é fazer a descoberta e pesquisa de dados de forma simples e robusta.

GraphQL consiste em duas partes: um sistema de tipo no qual descreve o esquema de dados que está disponível em nosso servidor e uma linguagem de consulta que permite a busca dos dados que necessitamos.

E quais são seus princípios de arquitetura?

  • Objetos Hierárquicos: o GraphQL trabalha para criação e manipulação de hierarquias de objetos. Uma consulta GraphQL é, em si, um conjunto hierárquico de campos. A consulta é moldada na maneira que você quer que os dados retornem.
  • Centrado no Produto: O GraphQL é pensado sempre como produto e suas necessidades; não precisamos nos preocupar em quais campos e como os campos são retornados, pois a consulta é montada conforme a especificação da view e não precisamos mexer em nada no nosso backend para retornar mais campos ou menos campos no mesmo endpoint.
  • Client Queries: Em GraphQL, as consultas são montadas no consumidor da nossa API em vez de ser montada no servidor. Na grande maioria dos aplicativos escritos sem GraphQL, o servidor determina os dados retornados em seus endpoints. Por outro lado, o GraphQL retorna exatamente o que o cliente pede sem precisarmos mexer em nosso backend.
  • Strongly-typed: GraphQL é fortemente tipado. Dada uma consulta, o sistema de tipo do GraphQL verifica os tipos e a sintaxe das Queries, validando as informações e garantido a qualidade dos dados.

Veja um exemplo de uma consulta construída em GraphQL:

{
  user(id: 3500401) {
    id,
    name,
    isViewerFriend,
    profilePicture(size: 50)  {
      uri,
      width,
      height
    }
  }
}

E a resposta do nosso servidor será essa aqui:

{
  "user" : {
    "id": 3500401,
    "name": "Jing Chen",
    "isViewerFriend": true,
    "profilePicture": {
      "uri": "http://admin.imasters.com.br/wp-content/uploads/2018/06/PIC.jpg",
      "width": 50,
      "height": 50
    }
  }
}

Construindo uma API em GraphQL

Para demonstrar o uso do GraphQL, criaremos um servidor de consulta em GraphQL utilizando .NET. O GraphQL está disponível em diversas linguagens, e claro, o .NET não poderia ficar para trás.

Para o nosso exemplo, crie um console application e após a criação do projeto vamos adicionar um pacote chamado GraphQL. Esse pacote é a implementação básica do GraphQL em .NET. Para adicionar o pacote, digite o comando abaixo no Package Manager Console:

  • Install-Package GraphQL
  • Microsoft.AspNet.WebApi.SelfHost

Nossa aplicação será um WebApi Self Host. Com isso vamos disponibilizar um endpoint de consulta em GraphQL Rest. Após a instalação dos pacotes, vamos configurar o Self Host.

Abra o arquivo program.cs e adicione o código abaixo:

static void Main(string[] args)
{
    Uri _baseAddress = new Uri("http://localhost:60064/");
    
    HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(_baseAddress);
    config.Routes.MapHttpRoute(
      name: "DefaultApi",
      routeTemplate: "api/{controller}/{id}",
      defaults: new { id = RouteParameter.Optional }
    );
    var bootstrapper = new Bootstrapper();
    config.DependencyResolver = bootstrapper.Resolver();
    // Create server
    var server = new HttpSelfHostServer(config);
    
    // Start listening
    server.OpenAsync().Wait();
    Console.WriteLine("Web API Self hosted on " + _baseAddress + " Hit ENTER to exit...");
    Console.ReadLine();
    server.CloseAsync().Wait();
}

Com o Self Host configurado, adicionaremos a injeção de dependência; o GraphQL necessita desse tipo de configuração para funcionar.

Configurando a injeção de dependência do GraphQL

Adicione um novo arquivo chamado Bootstrapper.cs; ele será o responsável por registrar nossas dependências.

public class Bootstrapper
   {
       public IDependencyResolver Resolver()
       {
           var container = BuildContainer();
           var resolver = new SimpleContainerDependencyResolver(container);
           return resolver;
       }
       private ISimpleContainer BuildContainer()
       {
           var container = new SimpleContainer();
           container.Singleton<IDocumentExecuter>(new DocumentExecuter());
           container.Singleton<IDocumentWriter>(new DocumentWriter(true));
           container.Singleton(new StarWarsData());
           container.Register<StarWarsQuery>();
           container.Register<HumanType>();
           container.Register<DroidType>();
           container.Register<CharacterInterface>();
           container.Singleton(new StarWarsSchema(type => (GraphType)container.Get(type)));
           return container;
       }
   }

Veja que temos classes que ainda não foram criadas – elas serão criadas ao longo desse artigo. Agora temos que criar nosso container de resolução de serviço.

Crie um arquivo chamado SimpleContainer.cs e adicione o código abaixo:

public interface ISimpleContainer : IDisposable
   {
       object Get(Type serviceType);
       T Get<T>();
       void Register<TService>();
       void Register<TService>(Func<TService> instanceCreator);
       void Register<TService, TImpl>() where TImpl : TService;
       void Singleton<TService>(TService instance);
       void Singleton<TService>(Func<TService> instanceCreator);
   }
   public class SimpleContainer : ISimpleContainer
   {
       private readonly Dictionary<Type, Func<object>> _registrations = new Dictionary<Type, Func<object>>();
       public void Register<TService>()
       {
           Register<TService, TService>();
       }
       public void Register<TService, TImpl>() where TImpl : TService
       {
           _registrations.Add(typeof(TService),
               () =>
               {
                   var implType = typeof(TImpl);
                   return typeof(TService) == implType
                       ? CreateInstance(implType)
                       : Get(implType);
               });
       }
       public void Register<TService>(Func<TService> instanceCreator)
       {
           _registrations.Add(typeof(TService), () => instanceCreator());
       }
       public void Singleton<TService>(TService instance)
       {
           _registrations.Add(typeof(TService), () => instance);
       }
       public void Singleton<TService>(Func<TService> instanceCreator)
       {
           var lazy = new Lazy<TService>(instanceCreator);
           Register(() => lazy.Value);
       }
       public T Get<T>()
       {
           return (T)Get(typeof(T));
       }
       public object Get(Type serviceType)
       {
           Func<object> creator;
           if (_registrations.TryGetValue(serviceType, out creator))
           {
               return creator();
           }
           if (!serviceType.IsAbstract)
           {
               return CreateInstance(serviceType);
           }
           throw new InvalidOperationException("No registration for " + serviceType);
       }
       public void Dispose()
       {
           _registrations.Clear();
       }
       private object CreateInstance(Type implementationType)
       {
           var ctor = implementationType.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
           var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType);
           var dependencies = parameterTypes.Select(Get).ToArray();
           return Activator.CreateInstance(implementationType, dependencies);
       }
   }

Esse é o arquivo responsável pela resolução dos nomes e pela a criação das instâncias dos objetos. Para finalizar, precisamos agora criar o Dependency Resolver. Adicione um novo arquivo chamado SimpleContainerDependencyResolver.cs e adicione o código abaixo:

public class SimpleContainerDependencyResolver : IDependencyResolver
    {
        private readonly ISimpleContainer _container;
        public SimpleContainerDependencyResolver(ISimpleContainer container)
        {
            _container = container;
        }
        public object GetService(Type serviceType)
        {
            try
            {
                return _container.Get(serviceType);
            }
            catch (Exception)
            {
                return null;
            }
        }
        public IEnumerable<object> GetServices(Type serviceType)
        {
            return Enumerable.Empty<object>();
        }
        public IDependencyScope BeginScope()
        {
            return this;
        }
        public void Dispose()
        {
        }
    }

Nossa injeção de dependência está configurada; agora temos a missão de configurar nosso schema GraphQL – esse é o ultimo passo para criar API Rest GraphQL.

Criando um schema GraphQL

Para que nossa API GraphQL funcione, precisamos configurar o nosso schema de consulta e seu objetos. Para esse artigo eu utilizei um exemplo de schema do StarWars, o mesmo usado pela biblioteca GraphQL.NET no seu exemplo.

A primeira coisa que devemos fazer é o Objeto de consulta; ele será o responsável por resolver nossas queries. Vamos adicionar um novo arquivo chamado StarWarsQuery.cs com o código abaixo:

public class StarWarsQuery : ObjectGraphType<object>
   {
       public StarWarsQuery(StarWarsData data)
       {
           
           Name = "Query";
           Field<CharacterInterface>("hero", resolve: context => data.GetDroidByIdAsync("3"));
           Field<HumanType>(
               "human",
               arguments: new QueryArguments(
                   new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "id", Description = "id of the human" }
               ),
               resolve: context => data.GetHumanByIdAsync(context.GetArgument<string>("id"))
           );
           Field<DroidType>(
               "droid",
               arguments: new QueryArguments(
                   new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "id", Description = "id of the droid" }
               ),
               resolve: context => data.GetDroidByIdAsync(context.GetArgument<string>("id"))
           );
       }
   }

Agora que o StarWarsQuery está pronto, precisamos adicionar nossos objetos que serão usados pela consulta. Para isso, adicione um novo arquivo chamado StarWarsCharacter – ele será o responsável pelo nosso modelo de dados.

public abstract class StarWarsCharacter
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string[] Friends { get; set; }
    public int[] AppearsIn { get; set; }
}
public class Human : StarWarsCharacter
{
    public string HomePlanet { get; set; }
}
public class Droid : StarWarsCharacter
{
    public string PrimaryFunction { get; set; }
}
public class EpisodeEnum : EnumerationGraphType
{
    public EpisodeEnum()
    {
        Name = "Episode";
        Description = "One of the films in the Star Wars Trilogy.";
        AddValue("NEWHOPE", "Released in 1977.", 4);
        AddValue("EMPIRE", "Released in 1980.", 5);
        AddValue("JEDI", "Released in 1983.", 6);
    }
}
public enum Episodes
{
    NEWHOPE = 4,
    EMPIRE = 5,
    JEDI = 6
}
#endregion
public class CharacterInterface : InterfaceGraphType<StarWarsCharacter>
{
    public CharacterInterface()
    {
        Name = "Character";
        Field(d => d.Id).Description("Id do Personagem");
        Field(d => d.Name, nullable: true).Description("Nome do Personagem");
        Field<ListGraphType<CharacterInterface>>("friends");
        Field<ListGraphType<EpisodeEnum>>("appearsIn", "Qual filme apareceu");
    }
}

Nosso modelo de dados está pronto – vamos criar um novo arquivo chamado StarWarsData, ele será responsável por fazer a consulta no banco de dados. Resumindo em uma aplicação real, seria ele a consultar nosso banco de dados.

public class StarWarsData
   {
       #region Types
       public class HumanType : ObjectGraphType<Human>
       {
           public HumanType(StarWarsData data)
           {
               Name = "Human";
               Field(h => h.Id).Description("The id of the human.");
               Field(h => h.Name, nullable: true).Description("The name of the human.");
               Field<ListGraphType<CharacterInterface>>(
                   "friends",
                   resolve: context => data.GetFriends(context.Source)
               );
               Field<ListGraphType<EpisodeEnum>>("appearsIn", "Which movie they appear in.");
               Field(h => h.HomePlanet, nullable: true).Description("The home planet of the human.");
               Interface<CharacterInterface>();
           }
       }
       public class DroidType : ObjectGraphType<Droid>
       {
           public DroidType(StarWarsData data)
           {
               Name = "Droid";
               Description = "A mechanical creature in the Star Wars universe.";
               Field(d => d.Id).Description("The id of the droid.");
               Field(d => d.Name, nullable: true).Description("The name of the droid.");
               Field<ListGraphType<CharacterInterface>>(
                   "friends",
                   resolve: context => data.GetFriends(context.Source)
               );
               Field<ListGraphType<EpisodeEnum>>("appearsIn", "Which movie they appear in.");
               Field(d => d.PrimaryFunction, nullable: true).Description("The primary function of the droid.");
               Interface<CharacterInterface>();
           }
       }
       #endregion
       private readonly List<Human> _humans = new List<Human>();
       private readonly List<Droid> _droids = new List<Droid>();
       public StarWarsData()
       {
           _humans.Add(new Human
           {
               Id = "1",
               Name = "Luke",
               Friends = new[] { "3", "4" },
               AppearsIn = new[] { 4, 5, 6 },
               HomePlanet = "Tatooine"
           });
           _humans.Add(new Human
           {
               Id = "2",
               Name = "Vader",
               AppearsIn = new[] { 4, 5, 6 },
               HomePlanet = "Tatooine"
           });
           _droids.Add(new Droid
           {
               Id = "3",
               Name = "R2-D2",
               Friends = new[] { "1", "4" },
               AppearsIn = new[] { 4, 5, 6 },
               PrimaryFunction = "Astromech"
           });
           _droids.Add(new Droid
           {
               Id = "4",
               Name = "C-3PO",
               AppearsIn = new[] { 4, 5, 6 },
               PrimaryFunction = "Protocol"
           });
       }
       public IEnumerable<StarWarsCharacter> GetFriends(StarWarsCharacter character)
       {
           if (character == null)
           {
               return null;
           }
           var friends = new List<StarWarsCharacter>();
           var lookup = character.Friends;
           if (lookup != null)
           {
               friends.AddRange(_humans.Where(h => lookup.Contains(h.Id)));
               friends.AddRange(_droids.Where(d => lookup.Contains(d.Id))); 
           }
           return friends;
       }
       public Task<Human> GetHumanByIdAsync(string id)
       {
           return Task.FromResult(_humans.FirstOrDefault(h => h.Id == id));
       }
       public Task<Droid> GetDroidByIdAsync(string id)
       {
           return Task.FromResult(_droids.FirstOrDefault(h => h.Id == id));
       }
   }

Por último e não menos importante, é o StarWarsSchema, ele é o responsável pelo gerenciamento do nosso schema. Adicione um novo arquivo chamado StarWarsSchema.cs e coloque o código abaixo:

public class StarWarsSchema : Schema
    {
        public StarWarsSchema(Func<Type, GraphType> resolveType)
            : base(resolveType)
        {
            Query = (StarWarsQuery)resolveType(typeof(StarWarsQuery));
        }
    }

Agora nosso Schema GraphQL e nosso modelo de dados estão prontos. Vamos expor uma API Rest para consultarmos os dados.

Criando uma WebApi GraphQL

Para expor nosso modelo de dados precisamos criar um WebApi. Vamos criar um novo arquivo chamado GraphQLController e adicionaremos o código abaixo:

public class GraphQLController : ApiController
  {
      private readonly ISchema _schema;
      private readonly IDocumentExecuter _executer;
      private readonly IDocumentWriter _writer;
      public GraphQLController(
          IDocumentExecuter executer,
          IDocumentWriter writer,
          StarWarsSchema schema)
      {
          _executer = executer;
          _writer = writer;
          _schema = schema;
        
      }
      [HttpGet]
      public Task<HttpResponseMessage> GetAsync(HttpRequestMessage request)
      {
          try
          {
              return PostAsync(request, new GraphQLQuery { Query = "query { hero { id,  name } }", Variables = "" });
          }
          catch (Exception ex)
          {
              return null;
          }
      }
      [HttpPost]
      public async Task<HttpResponseMessage> PostAsync(HttpRequestMessage request, GraphQLQuery query)
      {
          var inputs = query.Variables.ToInputs();
          var queryToExecute = query.Query;
          var result = await this._executer.ExecuteAsync(_ =>
          {
              _.Schema = _schema;
              _.Query = queryToExecute;
              _.OperationName = query.OperationName;
              _.Inputs = inputs;
              _.ComplexityConfiguration = new ComplexityConfiguration { MaxDepth = 15 };
              _.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
          }).ConfigureAwait(false);
          var httpResult = result.Errors?.Count > 0
              ? HttpStatusCode.BadRequest
              : HttpStatusCode.OK;
          var json = this._writer.Write(result);
          var response = request.CreateResponse(httpResult);
          response.Content = new StringContent(json, Encoding.UTF8, "application/json");
          return response;
      }
  }
  public class GraphQLQuery
  {
      public string OperationName { get; set; }
      public string NamedQuery { get; set; }
      public string Query { get; set; }
      public string Variables { get; set; }
  }

Agora está tudo pronto para nossa API Graph funcionar. Vamos testar.

Fazendo consulta na API GraphQL

Para testar nossa API, vamos utilizar o Postman, ele é uma extensão do Chrome, o qual nos permite realizar requisições em API Rest. Para testar nossa aplicação, vamos executar nosso projeto no Visual Studio apertando F5. Quando estiver rodando, abra a extensão Postman e realize uma consulta conforme mostrada na imagem abaixo. Se tudo estiver correto, você deverá ver uma resposta igual a minha.

Observação: Execute o Visual Studio em modo Administrador

O GraphQL é uma ótima alternativa para criarmos API Rest. Com poucas linha de códigos conseguimos ter uma API funcional, extensiva e robusta, e o melhor: trazemos somente os campos que foram solicitados, diminuindo assim o tráfego de rede.

Para mais informações sobre o GraphQL, veja a sua documentação clicando aqui.

O código deste exemplo está em meu GitHub através deste link.

Abraços e até a próxima!