Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/graphql #312

Merged
merged 7 commits into from Apr 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/features/graphql.rst
@@ -0,0 +1,15 @@
GraphQL
=======

OK you got me Ocelot doesn't directly support GraphQL but so many people have asked about it I wanted to show how easy it is to integrate
the `graphql-dotnet <https://github.com/graphql-dotnet/graphql-dotnet>`_ library.


Please see the sample project `OcelotGraphQL <https://github.com/ThreeMammals/Ocelot/tree/develop/samples/OcelotGraphQL>`_.
Using a combination of the graphql-dotnet project and Ocelot's DelegatingHandler features this is pretty easy to do.
However I do not intend to integrate more closely with GraphQL at the moment. Check out the samples readme and that should give
you enough instruction on how to do this!

Good luck and have fun :>


122 changes: 107 additions & 15 deletions docs/features/requestaggregation.rst
Expand Up @@ -5,12 +5,114 @@ Ocelot allow's you to specify Aggregate ReRoutes that compose multiple normal Re
a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type
architecture with Ocelot.

This feature was requested as part of `Issue 79 <https://github.com/TomPallister/Ocelot/pull/79>`_ .
This feature was requested as part of `Issue 79 <https://github.com/TomPallister/Ocelot/pull/79>`_ and further improvements were made as part of `Issue 298 <https://github.com/TomPallister/Ocelot/issue/298>`_.

In order to set this up you must do something like the following in your configuration.json. Here we have specified two normal ReRoutes and each one has a Key property.
We then specify an Aggregate that composes the two ReRoutes using their keys in the ReRouteKeys list and says then we have the UpstreamPathTemplate which works like a normal ReRoute.
Obviously you cannot have duplicate UpstreamPathTemplates between ReRoutes and Aggregates. You can use all of Ocelot's normal ReRoute options apart from RequestIdKey (explained in gotchas below).

Advanced register your own Aggregators
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Ocelot started with just the basic request aggregation and since then we have added a more advanced method that let's the user take in the responses from the
downstream services and then aggregate them into a response object.

The configuration.json setup is pretty much the same as the basic aggregation approach apart from you need to add an Aggregator property like below.

.. code-block:: json

{
"ReRoutes": [
{
"DownstreamPathTemplate": "/",
"UpstreamPathTemplate": "/laura",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 51881
}
],
"Key": "Laura"
},
{
"DownstreamPathTemplate": "/",
"UpstreamPathTemplate": "/tom",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 51882
}
],
"Key": "Tom"
}
],
"Aggregates": [
{
"ReRouteKeys": [
"Tom",
"Laura"
],
"UpstreamPathTemplate": "/",
"Aggregator": "FakeDefinedAggregator"
}
]
}

Here we have added an aggregator called FakeDefinedAggregator. Ocelot is going to look for this aggregator when it tries to aggregate this ReRoute.

In order to make the aggregator available we must add the FakeDefinedAggregator to the OcelotBuilder like below.

.. code-block:: csharp

services
.AddOcelot()
.AddSingletonDefinedAggregator<FakeDefinedAggregator>();

Now when Ocelot tries to aggregate the ReRoute above it will find the FakeDefinedAggregator in the container and use it to aggregate the ReRoute.
Because the FakeDefinedAggregator is registered in the container you can add any dependencies it needs into the container like below.

.. code-block:: csharp

services.AddSingleton<FooDependency>();

services
.AddOcelot()
.AddSingletonDefinedAggregator<FooAggregator>();

In this example FooAggregator takes a dependency on FooDependency and it will be resolved by the container.

In addition to this Ocelot lets you add transient aggregators like below.

.. code-block:: csharp

services
.AddOcelot()
.AddTransientDefinedAggregator<FakeDefinedAggregator>();

In order to make an Aggregator you must implement this interface.

.. code-block:: csharp

public interface IDefinedAggregator
{
Task<DownstreamResponse> Aggregate(List<DownstreamResponse> responses);
}

With this feature you can pretty much do whatever you want because DownstreamResponse contains Content, Headers and Status Code. We can add extra things if needed
just raise an issue on GitHub. Please note if the HttpClient throws an exception when making a request to a ReRoute in the aggregate then you will not get a DownstreamResponse for
it but you would for any that succeed. If it does throw an exception this will be logged.

Basic expecting JSON from Downstream Services
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: json

{
Expand Down Expand Up @@ -65,9 +167,6 @@ If the ReRoute /tom returned a body of {"Age": 19} and /laura returned {"Age": 2

{"Tom":{"Age": 19},"Laura":{"Age": 25}}

Gotcha's / Further info
^^^^^^^^^^^^^^^^^^^^^^^

At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a json dictionary
as above. With the ReRoute key being the key of the dictionary and the value the response body from your downstream service. You can see that the object is just
JSON without any pretty spaces etc.
Expand All @@ -76,20 +175,13 @@ All headers will be lost from the downstream services response.

Ocelot will always return content type application/json with an aggregate request.

You cannot use ReRoutes with specific RequestIdKeys as this would be crazy complicated to track.

Aggregation only supports the GET HTTP Verb.

If you downstream services return a 404 the aggregate will just return nothing for that downstream service.
It will not change the aggregate response into a 404 even if all the downstreams return a 404.

Future
^^^^^^
Gotcha's / Further info
-----------------------

There are loads of cool ways to enchance this such as..
You cannot use ReRoutes with specific RequestIdKeys as this would be crazy complicated to track.

What happens when downstream goes slow..should we timeout?
Can we do something like GraphQL where the user chooses what fields are returned?
Can we handle 404 better etc?
Can we make this not just support a JSON dictionary response?
Aggregation only supports the GET HTTP Verb.

1 change: 1 addition & 0 deletions docs/index.rst
Expand Up @@ -21,6 +21,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n
features/configuration
features/routing
features/requestaggregation
features/graphql
features/servicediscovery
features/servicefabric
features/authentication
Expand Down
19 changes: 15 additions & 4 deletions docs/introduction/notsupported.rst
Expand Up @@ -7,7 +7,10 @@ Ocelot does not support...

* Fowarding a host header - The host header that you send to Ocelot will not be forwarded to the downstream service. Obviously this would break everything :(

* Swagger - I have looked multiple times at building swagger.json out of the Ocelot configuration.json but it doesnt fit into the vision I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore
* Swagger - I have looked multiple times at building swagger.json out of the Ocelot configuration.json but it doesnt fit into the vision
I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your
Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns
it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore

.. code-block:: csharp

Expand All @@ -25,8 +28,16 @@ Ocelot does not support...

app.UseOcelot().Wait();

The main reasons why I don't think Swagger makes sense is we already hand roll our definition in configuration.json. If we want people developing against Ocelot to be able to see what routes are available then either share the configuration.json with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration.
The main reasons why I don't think Swagger makes sense is we already hand roll our definition in configuration.json.
If we want people developing against Ocelot to be able to see what routes are available then either share the configuration.json
with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration.

In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to there product service and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot information would not match. Unless I rolled my own Swagger implementation.
In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to there product service
and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has
no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return
multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle
package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot
information would not match. Unless I rolled my own Swagger implementation.

If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might even be possible to write something that maps configuration.json to the postman json spec. However I don't intend to do this.
If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might
even be possible to write something that maps configuration.json to the postman json spec. However I don't intend to do this.
18 changes: 18 additions & 0 deletions samples/OcelotGraphQL/OcelotGraphQL.csproj
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Update="configuration.json;appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6"/>
<PackageReference Include="Ocelot" Version="5.5.1"/>
<PackageReference Include="GraphQL" Version="2.0.0-alpha-870"/>
</ItemGroup>
</Project>
131 changes: 131 additions & 0 deletions samples/OcelotGraphQL/Program.cs
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Ocelot.Middleware;
using Ocelot.DependencyInjection;
using GraphQL.Types;
using GraphQL;
using Ocelot.Requester;
using Ocelot.Responses;
using System.Net.Http;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using System.Threading;

namespace OcelotGraphQL
{
public class Hero
{
public int Id { get; set; }
public string Name { get; set; }
}

public class Query
{
private List<Hero> _heroes = new List<Hero>
{
new Hero { Id = 1, Name = "R2-D2" },
new Hero { Id = 2, Name = "Batman" },
new Hero { Id = 3, Name = "Wonder Woman" },
new Hero { Id = 4, Name = "Tom Pallister" }
};

[GraphQLMetadata("hero")]
public Hero GetHero(int id)
{
return _heroes.FirstOrDefault(x => x.Id == id);
}
}

public class GraphQlDelegatingHandler : DelegatingHandler
{
private readonly ISchema _schema;

public GraphQlDelegatingHandler(ISchema schema)
{
_schema = schema;
}

protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//try get query from body, could check http method :)
var query = await request.Content.ReadAsStringAsync();

//if not body try query string, dont hack like this in real world..
if(query.Length == 0)
{
var decoded = WebUtility.UrlDecode(request.RequestUri.Query);
query = decoded.Replace("?query=", "");
}

var result = _schema.Execute(_ =>
{
_.Query = query;
});

//maybe check for errors and headers etc in real world?
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(result)
};

//ocelot will treat this like any other http request...
return response;
}
}

public class Program
{
public static void Main(string[] args)
{
var schema = Schema.For(@"
type Hero {
id: Int
name: String
}

type Query {
hero(id: Int): Hero
}
", _ => {
_.Types.Include<Query>();
});

new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
config
.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
.AddJsonFile("configuration.json")
.AddEnvironmentVariables();
})
.ConfigureServices(s => {
s.AddSingleton<ISchema>(schema);
s.AddOcelot()
.AddSingletonDelegatingHandler<GraphQlDelegatingHandler>();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
})
.UseIISIntegration()
.Configure(app =>
{
app.UseOcelot().Wait();
})
.Build()
.Run();
}
}
}