Building a quick Reddit Blazor client without Reddit's API

When developing the new exception landing pages we recently launched (like insert exception link here), I wanted to pull some statistics from Reddit. While looking through various ways to integrate, I found an easy approach that I want to share with you in this post.

Building a quick Reddit Blazor client without Reddit's API

You probably already know Reddit, the highly active social news aggregation and discussion forum. I've found myself using Reddit more and more over the last couple of years, with the dotnet subreddit in particular. Reddit offers both an excellent website and a mobile app, but what if you want to pull Reddit data from C#? Examples could be pulling statistics like I've done for the exception pages or creating a monitoring tool that stores conversations about specific keywords.

The obvious choice to create the integration would be Reddit's API. There are a lot of endpoints and my guess is that you could build a really awesome fully-featured Reddit client using this API. I do require a user and OAuth authentication. For clients that don't need anything else than search and can live with data being a bit outdated, I found pushshift.io.

pushshift.io is a Reddit search API designed and created by the datasets mod team. It is based on Elasticsearch and hence provides great search and aggregation capabilities on top of Reddit data. But enough talk, let's start coding. To demonstrate how to use pushshift.io, I'll build a simple Reddit client as a Blazor application. As a quick disclaimer, I'm far from an expert developing Blazor apps. The code in this post is meant as a way to showcase the pushshift.io integration only and not something that can be considered best practice in any way 😉 Start by creating a new Blazor WebAssembly App:

Configure your new project

I'm going with all default settings:

Additional information

After the creation and launching the project through Visual Studio, we see the normal Blazor template in action:

Default Blazor template

So far so good. For this example, I'll create a new page in the default template. To do so, start by adding a new menu item by including the following code in the NavMenu.razor file:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="reddit">
        <span class="oi oi-browser" aria-hidden="true"></span> Reddit
    </NavLink>
</div>

Next, add a new file named RedditClient.razor to the Pages folder. Include the following code:

@page "/reddit"
@using System.Text.Json.Nodes
@inject HttpClient Http

<PageTitle>Reddit</PageTitle>

<h1>Reddit</h1>

This will make a page available when navigating to the /reddit URL. I've added a using of the System.Text.Json.Nodes namespace which we will need in a few seconds. I have also injected a HttpClient which will be used to communicate with the pushshift.io API.

For the UI, include a search field and components to show both a search result and the selected Reddit submission:

@*Search field*@
<input @oninput="Search" />

@*Results table*@
<table class="table">
    <thead>
        <tr>
            <th>Time</th>
            <th>Title</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var result in Results)
        {
            <tr>
                <td>@result.Time</td>
                <td>@result.Title</td>
                <td><button @onclick="() => OpenDetails(result)">Details</button></td>
            </tr>
        }
    </tbody>
</table>

@*Selected submission*@
<dl class="row">
    <dt class="col-1">Time:</dt>
    <dd class="col-11">@SelectedTime</dd>
    <dt class="col-1">Title:</dt>
    <dd class="col-11">@SelectedTitle</dd>
    <dt class="col-1">Text:</dt>
    <dd class="col-11">@SelectedText</dd>
</dl>

If you haven't seen Razor syntax before, let's go through each section. The first part is an HTML input field where I bind the value to a (yet to be defined) method named Search. The results table will generate a list of search results showing a time, time, and a button to see more details. The last section shows the time, title, and text of the selected search result.

Start by adding a code section to the bottom of the razor file and include the properties that the view data-bind to:

@code {
    private DateTimeOffset? SelectedTime { get; set; }
    private string SelectedTitle { get; set; }
    private string SelectedText { get; set; }

    private List<Result> Results { get; set; } = new List<Result>();

    private class Result
    {
        public DateTimeOffset? Time { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }
    }
}

Next, we need the Search method to trigger every time the user inputs a value in the input field:

private async Task Search(ChangeEventArgs args)
{
    Results.Clear();
    var result = await Http.GetAsync($"https://api.pushshift.io/reddit/submission/search?subreddit=dotnet&sort=desc&sort_type=created_utc&size=10&q={args.Value}");
    var response = await result.Content.ReadAsStringAsync();
    var json = JsonNode.Parse(response);
    dynamic data = json["data"];
    var toAdd = new List<dynamic>();
    foreach (var submission in data)
    {
        Results.Add(new Result
        {
            Time = DateTimeOffset.FromUnixTimeSeconds(submission["created_utc"].GetValue<long>()),
            Title = submission["title"].GetValue<string>(),
            Text = submission["selftext"].GetValue<string>()
        });
    }
}

As shown in the code, the method uses the injected HttpClient to call the pushshift.io API. I use the reddit/submission/search path to only search submissions. In the query parameters, there are a lot of available options to filter and aggregate results. I'm limiting the search results to the dotnet subreddit to make the results relevant to .NET. Then I sort the results by created date and limit the number of results to 10. Finally, I add the inputted value to the q query parameters which is the text to search for.

In the following lines, I parse the results and map them to Result objects. There are probably smarter ways of doing this using System.Text.Json, but let's not focus too much on that now.

The only missing part is defining the OpenDetails method that you may remember being called when clicking the button in each search result:

private async Task OpenDetails(Result result)
{
    SelectedTime = result.Time;
    SelectedTitle = result.Title;
    SelectedText = result.Text;
}

No magic here. The method set the three properties that the UI is data binding to in the third section.

Let's launch the application and input a query:

Reddit Blazor client

Yay! Our very own Reddit client. Here's the full code:

@page "/reddit"
@using System.Text.Json.Nodes
@inject HttpClient Http

<PageTitle>Reddit</PageTitle>

<h1>Reddit</h1>

@*Search field*@
<input @oninput="Search" />

<table class="table">
    <thead>
        <tr>
            <th>Time</th>
            <th>Title</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var result in Results)
        {
            <tr>
                <td>@result.Time</td>
                <td>@result.Title</td>
                <td><button @onclick="() => OpenDetails(result)">Details</button></td>
            </tr>
        }
    </tbody>
</table>

<dl class="row">
    <dt class="col-1">Time:</dt>
    <dd class="col-11">@SelectedTime</dd>
    <dt class="col-1">Title:</dt>
    <dd class="col-11">@SelectedTitle</dd>
    <dt class="col-1">Text:</dt>
    <dd class="col-11">@SelectedText</dd>
</dl>

@code {
    private DateTimeOffset? SelectedTime { get; set; }
    private string SelectedTitle { get; set; }
    private string SelectedText { get; set; }

    private List<Result> Results { get; set; } = new List<Result>();

    private class Result
    {
        public DateTimeOffset? Time { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }
    }

    private async Task OpenDetails(Result result)
    {
        SelectedTime = result.Time;
        SelectedTitle = result.Title;
        SelectedText = result.Text;
    }

    private async Task Search(ChangeEventArgs args)
    {
        Results.Clear();
        var result = await Http.GetAsync($"https://api.pushshift.io/reddit/submission/search?subreddit=dotnet&sort=desc&sort_type=created_utc&size=10&q={args.Value}");
        var response = await result.Content.ReadAsStringAsync();
        var json = JsonNode.Parse(response);
        dynamic data = json["data"];
        var toAdd = new List<dynamic>();
        foreach (var submission in data)
        {
            Results.Add(new Result
            {
                Time = DateTimeOffset.FromUnixTimeSeconds(submission["created_utc"].GetValue<long>()),
                Title = submission["title"].GetValue<string>(),
                Text = submission["selftext"].GetValue<string>()
            });
        }
    }
}

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website