DEV Community

Steve Lorello
Steve Lorello

Posted on

Querying Redis with GraphQL using Redis OM .NET

Note: This post is part of C# Advent 2023, to check out the entire series check out the whole Calender Big shoutout to @mgroves for organizing.

Earlier this year I received an issue whereby one of our users had taken to extending Redis OM .NET to integrate with GraphQL and was experiencing difficulty. Redis OM .NET is a LINQ-based Redis Search and Query client.

Now, integrating with GraphQL is not a supported feature, but given that the user had contributed to the library in the past, and offered up their own PR, I figured I ought to take a look. Now, I'm not by any stretch of the imagination a GrpahQL expert, but by golly the thing seemed to actually work. Not only that, but it seemed to work with minimal intervention needed from the user. So in this post I'm going to lay out what you need to do to get GraphQL issuing well-formed Redis Queries.

Note: this is not offically supported by Redis OM .NET
Big shoutout to Rohan Edman whose issue inspired this post.

Prerequisties

Start Redis Stack

First and foremost we need to start our Redis Stack instance, just run docker run -p 6379:6379 redis/redis-stack-server or otherwise your favorite way to run Redis Stack.

Jump straight to the code

All the code for this project can be found by cloning this Repo.

git clone https://github.com/slorello89/redis-om-graphql-example
Enter fullscreen mode Exit fullscreen mode

You can just clone that and

Integrating HotChocolate with Redis OM .NET

Hot Chocolate is part of the Chilli Cream GraphQL platform that serves as a GraphQL Server for .NET instances. That's what we'll be using to esentially serve as the first step of middleware in our translation layer between GraphQL and Redis.

Create the project

Create the project by running:

dotnet new web -n GraphQL
Enter fullscreen mode Exit fullscreen mode

And changing directories into the project.

cd GraphQL
Enter fullscreen mode Exit fullscreen mode

Add Redis.OM and Hot Chocloate Packages

Now you just need to add the hot chocloate and Redis OM Packages:

dotnet add package Redis.OM
dotnet add package HotChocolate.AspNetCore --version 13.7.0
dotnet add package HotChocolate.Data --version 13.7.0
Enter fullscreen mode Exit fullscreen mode

Fetch the data needed for this project

In the source Repo, there's a folder called data, download these json files and add them to a new directory in the root of your project called data. Then in the .csproj file add the following directive to see that they are copied to the build directory:

<ItemGroup>
    <Content Include="./data/*.*">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Create Model

We are going to be inserting a bunch of book data into Redis, we need to add a simple data model to do this:

using Redis.OM.Modeling;

namespace GraphQL;

[Document(StorageType = StorageType.Json)]
public class Book
{
    [Indexed]
    [RedisIdField]
    public string? Id { get; set; }

    [Searchable]
    public string? Title { get; set; }

    [Searchable]
    public string? SubTitle { get; set; }

    [Searchable]
    public string? Description { get; set; }

    [Indexed]
    public string? Language { get; set; }

    [Indexed]
    public long? PageCount { get; set; }

    [Indexed]
    public string? Thumbnail { get; set; }

    [Indexed]
    public double? Price { get; set; }

    [Indexed]
    public string? Currency { get; set; }

    public string? InfoLink { get; set; }

    [Indexed]
    public string[]? Authors { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

In Redis Terms, the Indexed attributes indicate that they will be stored as tag fields for Exact matches, and Searchable indicates that the field will be stored as a Text field for full-text search.

Create Bootstrapping Service

We will add an IHostedService to seed Redis with all the necessary data. Create a file called StartupService.cs and add the following:

using System.Text.Json;
using Redis.OM;
using Redis.OM.Contracts;

namespace GraphQL;

public class StartupService : IHostedService
{
    private readonly IRedisConnectionProvider _provider;

    public StartupService(IRedisConnectionProvider provider)
    {
        _provider = provider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var books = _provider.RedisCollection<Book>();
        _provider.Connection.DropIndexAndAssociatedRecords(typeof(Book));
        await _provider.Connection.CreateIndexAsync(typeof(Book));

        var files = Directory.GetFiles("data");
        foreach (var file in files)
        {
            var tasks = new List<Task>();
            var str = await File.ReadAllTextAsync(file, cancellationToken);
            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
            var booksInFile = JsonSerializer.Deserialize<Book[]>(str, options);
            foreach (var book in booksInFile)
            {
                tasks.Add(books.InsertAsync(book));
            }

            await Task.WhenAll(tasks);
        }

    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Enter fullscreen mode Exit fullscreen mode

At startup this will:

  1. Drop the Books index and anything that was in it.
  2. Create the Books index
  3. Insert all the books read out of our json files in the data directory into Redis

Create Query Class

Hot Chocolate requires you to have a query class to perform the queries on your data. Let's just crate a file Query.cs and add the following to it:

using HotChocolate.Resolvers;
using Redis.OM.Contracts;
using Redis.OM.Searching;

namespace GraphQL;

[ExtendObjectType("Query")]
public class Query
{
    private readonly IRedisCollection<Book> _books;

    public Query(IRedisConnectionProvider provider)
    {
        _books = provider.RedisCollection<Book>();
    }

    [UsePaging(IncludeTotalCount = true, MaxPageSize = 10)]
    [UseProjection]
    [UseFiltering]
    public IQueryable<Book> GetBook(IResolverContext context) => _books.Filter(context);
}
Enter fullscreen mode Exit fullscreen mode

In here, we are depenency injecting an IRedisConnectionProvider to create the Books collection, and then we are simply calling the extension Filter on our Books collection using the ResolverContext, that comes from HotChocolate tellin the IQueryable _books how to query for books in Redis. the IRedisCollection<Book> is an IQueryable that just knows how to properly parse the expressions it's provided by Hot Chocolate into a usable RediSearch Query.

Configure our Services / App

Finally, in Program.cs we need to depenency inject our IRedisConnectionProvider:

builder.Services.AddSingleton<IRedisConnectionProvider>(new RedisConnectionProvider("redis://localhost:6379"));
Enter fullscreen mode Exit fullscreen mode

then we need to add our StartupService:

builder.Services.AddHostedService<StartupService>();
Enter fullscreen mode Exit fullscreen mode

then we need to configure our GrpahQL Service:

builder.Services.AddGraphQLServer()
    .AddProjections()
    .AddFiltering()
    .AddQueryType(d => d.Name("Query"))
    .AddType<Query>();
Enter fullscreen mode Exit fullscreen mode

Notice how we are adding projection, filtering, and our query type.

Then we need to build our app and map our GraphQL service:

var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.MapGraphQL();
app.Run();
Enter fullscreen mode Exit fullscreen mode

And that's it!

Start the Project

To start the project just run dotnet run from the directory you clone the GitHub repo into.

The exact endpoint that it will take on will displayed in your terminal, in my case the endpoint is http://localhost:5097

Open up your GraphQL GUI of choice, and point it at <Your-endpoint>/graphql, et voila, you have a graphQL endpoint that will faithfully take your GraphQL queries and attempt to parse them as Redis Queries. take the following example:

Banana Cake Pop GUI

This query:

query {
  book(where: { description: { eq: "Redis" } pageCount: {lt: 100}}) {
    nodes {
      title
      subTitle
      description
      pageCount, 
      infoLink
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Is translated to:

"FT.SEARCH" "book-idx" "((@PageCount:[-inf (100]) (@Description:\"Redis\"))" "LIMIT" "0" "11" "RETURN" "15" "Title" "AS" "Title" "SubTitle" "AS" "SubTitle" "Description" "AS" "Description" "PageCount" "AS" "PageCount" "$.InfoLink" "AS" "InfoLink"
Enter fullscreen mode Exit fullscreen mode

In Redis, which is exactly what you would expect. and it does all of this with what minimal code and effort.

Resources

  • The code for this post is all in its Repo
  • You can Learn more about Hot Chocolate on their website
  • To learn more about Redis OM, visit its Repo
  • To learn more about Redis, visit redis.io

Top comments (0)