DEV Community

Myles Broomes
Myles Broomes

Posted on

Personal Project: Backlog Tracker

As someone who doesn't build nearly enough of my own projects, I thought I would try my hand at building something fun, using as many of the skills that I know as I could. I wanted to build something that I would actually use, and as someone who plays a lot of video games, I thought it'd be cool to create a web application that is able to allow users to track which games they're currently playing, which games they plan to play and which games they've completed. A "Backlog Tracker", if you will.

Project Management

The first step was to decide upon on the MVP (or Minimum Viable Project). The three most important features I decided that were needed were:

  • User's should be able to register an account
  • User's should be able to search against a large database of games
  • User's should be able to add and remove games to and from their backlog

The next step was then to decide what I would use to manage project tasks. I went with Trello as it would allow me to create cards for each individual task. I can also add labels and descriptions to each card to make it easier to organize them:

Trello Demo

Building The Project

I made the decision to build the project using .NET as that's what I'm most comfortable using, as I use it in my day job.

It was then finally time to get my hands dirty with some coding. The frontend of the application is built using .NET Razor Pages. Razor Pages are split into a razor page containing the razor markup, and a C# class (known as the page model) where any necessary logic is included. For example, in this project the homepage is a simple page that, initially, just contains a search bar. Here is how the razor markup for that page looks:

@page
@using Microsoft.AspNetCore.Html
@model IndexModel
@{
    ViewData["Title"] = "Home page";
    var user = Model.User.Identity;
}

<div class="text-center">
    <h1 class="display-4">Backlog Tracker</h1>
</div>

<div id="errorMessage"></div>

<form method="post" style="margin-bottom:1rem">
    <div class="input-group">
        <input class="form-control" asp-for="SearchTerm" name="query">
        <div class="input-group-append">
            <button class="btn btn-primary" type="submit">Search</button>
        </div>
    </div>
</form>

@if (Model.GamesResponse?.Games is { Count: > 0 })
{
    <div class="row row-cols-4">
        @foreach (var game in Model.GamesResponse.Games)
        {
            <div class="col">
                <div class="card game-card">
                    <div class="card-body">
                        <h5 class="card-title">
                            <a href="@game.SiteDetailUrl" target="_blank">@game.Name</a>
                        </h5>
                        @if (!string.IsNullOrEmpty(game.Description))
                        {
                            <div class="card-text-container">
                                <p class="card-text">@(new HtmlString(game.Description))</p>
                            </div>
                        }
                        @if (user !=null && user.IsAuthenticated)
                        {
                            <a class="btn btn-primary" id="btn_@game.Id" data-game-id="@game.Id" data-game-name="@game.Name" onclick="addToBacklog(this,'@user?.Name?.Trim()')">+</a>
                        }
                    </div>
                </div>
            </div>
        }
    </div>
}
else if (Model.GamesResponse?.Games is { Count: 0 })
{
    <p>@ViewData["NoResults"]</p>
}
Enter fullscreen mode Exit fullscreen mode

The Model object is the page model which is processed in a separate file. Entering text into the search bar and hitting enter, makes a post request (handled by the OnPost() method in the C# page model), which makes an API call to the Giant Bomb API (more on that later) and then displays relevant data on the page:

public IndexModel(IGameService gameService, ILogger<IndexModel> logger)
{
    _gameService = gameService;
    _logger = logger;
}

public async void OnPost()
{            
    string? query = Request.Form["query"];

    if (!string.IsNullOrEmpty(query))
    { 
        try
        {
            _logger.LogInformation("Fetching games from GiantBomb API...");
            GamesResponse = await _gameService.GetGamesAsync(Request.Form["query"]);
            ViewData["NoResults"] = GamesResponse == null ? "" : "No Games Matching The Query!";
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Error occurred while fetching games - {ex.Message}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

All logic that involves interacting directly with the Giant Bomb API is contained in the GiantBombService. As you can see, I inject the service into the constructor (using dependency injection) of the page model and then use it to call GetGamesAsync. The data from this API call is stored in the GamesReponse property (which is an object of type Response) on the page model class, which can then be used on the frontend.

I needed an API which would allow me to pull from a database containing information about video games so, after a bit of research, I came across the Giant Bomb API which does exactly what I would need. The user would search for a game within my application, which would request data about said game from the API then the application can handle the returned data as needed.

public async Task<Response> GetGamesAsync(string? query)
{
    var games = new Response();

    var client = _httpClientFactory.CreateClient("GiantBomb");

    var response = client.GetAsync($"/api/search/?api_key={_giantBombConfiguration.Value.GiantBombAPIKey}&query={query}&resources=game&field_list=name,site_detail_url,description,id").Result;

    XmlSerializer xs = new XmlSerializer(typeof(Response));

    using (StreamReader reader = new StreamReader(await response.Content.ReadAsStreamAsync()))
    {
        try
        {
            games = (Response?)xs.Deserialize(reader);
            _logger.LogInformation("Deserialization complete!");
        }
        catch (Exception ex)
        {
            _logger.LogError($"Error occurred during deserialization: {ex.Message}");
        }
    }

    _logger.LogInformation("Request complete!");

    return games ?? new Response();
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the data we receive from the API is in XML format so we need to deserialize that before being able to use it.

I needed a way of persisting user data so I decided to use Entity Framework and .NET Identity. This took out a lot of the complexity around doing things like setting up my own SQL database and building my own user authentication functionality.

The user would need to be able to add game data to their backlog - once signed in, they'd have the ability to click a button next to each game which would add it to their backlog. I decided to create my own .NET Web API to handle this. There is a JavaScript event listener on each button which would call my API and handle updating that user's backlog in the database. Here is an example of how that process works for the "AddToBacklog()" method:

@if (Model.GamesResponse?.Games is { Count: > 0 })
{
    <div class="row row-cols-4">
        @foreach (var game in Model.GamesResponse.Games)
        {
            <div class="col">
                <div class="card game-card">
                    <div class="card-body">
                        <h5 class="card-title">
                            <a href="@game.SiteDetailUrl" target="_blank">@game.Name</a>
                        </h5>
                        @if (!string.IsNullOrEmpty(game.Description))
                        {
                            <div class="card-text-container">
                                <p class="card-text">@(new HtmlString(game.Description))</p>
                            </div>
                        }
                        @if (user !=null && user.IsAuthenticated)
                        {
                            <a class="btn btn-primary" id="btn_@game.Id" data-game-id="@game.Id" data-game-name="@game.Name" onclick="addToBacklog(this,'@user?.Name?.Trim()')">+</a>
                        }
                    </div>
                </div>
            </div>
        }
    </div>
}
else if (Model.GamesResponse?.Games is { Count: 0 })
{
    <p>@ViewData["NoResults"]</p>
}
Enter fullscreen mode Exit fullscreen mode

This for loop adds onclick="addToBacklog(this,'@user?.Name?.Trim()') to each button, which calls the following function:

function addToBacklog(element, email) {
    const apiUrl = '/api/backlog/add-game-to-backlog';

    const requestData = {
        Email: email,
        GameID: element.getAttribute("data-game-id")
    };

    fetch(apiUrl, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(requestData)
    })
        .then(response => {
            if (response.ok)
                return response.json();
            else if (response.status == 409)
                throw new Error("Game already exists in backlog!");
            else
                throw new Error('Network response was not ok.')
        })
        .then(data => {
            alert(element.getAttribute("data-game-name") + " successfully added to backlog!");
        })
        .catch(error => {
            var errorMsg = document.getElementById("errorMessage");
            console.error('There was a problem sending data to the API:', error);
            errorMsg.innerHTML = error;
            errorMsg.style.visibility = 'visible';
        });

}
Enter fullscreen mode Exit fullscreen mode

/api/backlog/add-game-to-backlog is the location of my Web API which calls the AddToBacklog() method. It gets passed a UserDto object which is an object containing the email address of the current user and the ID of the game being added to the backlog:

[HttpPatch("add-game-to-backlog")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult AddToBacklog([FromBody] UserDto userDto)
{
    try
    {
        _backlogService.AddToBacklog(userDto);
        return Ok(new { Message = "Data saved successfully." });
    }
    catch (ArgumentException ex)
    {
        return Conflict(ex.Message);
    }
    catch (Exception ex)
    {
        return StatusCode(500, $"An error occurred while processing the request - {ex.Message}.");
    }            
}
Enter fullscreen mode Exit fullscreen mode

The BacklogService in this example is injected into this classes constructor (just like with the GiantBombService) and then can be used to handle interacting directly with the database. The AddToBacklog() method above is able to consume data from the request body of the API call and then send it off to the backlog service to be added to the database. A similar process is used when user's move games from their backlog to their completed games list. They click a button that has a JavaScript event listener, which makes a request to my API, which then moves the game from the backlog column in the database to the completed games column.

Unit Testing with xUnit

I wanted a way of incorporating unit testing into the project, so I decided to use xUnit and I have created a separate project which contains the test suite. Within these tests, I've mocked the backlog service which allowed me to just focus on the functionality being tested (the API controller in this case). For this, I used the Moq package and created the following object:

_backlogServiceMock = new Mock<IBacklogService>();

This then allowed me to mock each method from the service and how that method should behave when called in my test. For example, one of the things I wanted to test was, when the GetUsersBacklog() method is called in the API, it should return a valid list of game ID's when passed a specific email address:

_backlogServiceMock.Setup(s => s.GetBacklog("mylesbroomestest@hotmail.co.uk")).Returns(new List<string>() { "1234", "4567", "7890" });

This basically translates to when the string "mylesbroomestest@hotmail.co.uk" is passed to the GetBacklog() method, return a list of strings containing "1234", "4567" and "7890". Here's how it looks in action:

[Fact]
public void GetUsersBacklog_WhenCalled_ReturnsListOfGameIds()
{
    // Act
    var result = _controller.GetUsersBacklog("mylesbroomestest@hotmail.co.uk") as OkObjectResult;

    // Assert
    var gameIds = Assert.IsType<List<string>>(result?.Value);
    Assert.Equal(3, gameIds.Count);
}
Enter fullscreen mode Exit fullscreen mode

The gameIDs list

Overall, this was an extremely fun project to build. It required me to learn several things I didn't know already and I feel like I've really expanded my knowledge of .NET, working with API's, dependency injection and unit testing from working on it.

Here is the repository of the completed project if you want to see the code for yourself: https://github.com/MylesB93/backlog-tracker/tree/master.

Thanks for reading and happy coding! 👨🏾‍💻

Top comments (1)