Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Excelsior! Building Applications Without a Safety Net - Part 1

5.00/5 (4 votes)
11 May 2021CPOL28 min read 7.8K  
First part of a series of articles where we build an application showing the entire thought process when writing it
This is the first part in a series of articles in which I attempt to answer the question How much better would I be as a developer now, if I could have listened to the thought processes of other developers when they were writing applications?

Introduction

I remember, as a young developer, being in awe of people who could sit down and code, seemingly without any effort. Systems would seem to flow out of their fingers, effortlessly crafted, elegant, and refined. It felt like I was witnessing Michelangelo in the Sistine Chapel or Mozart sitting down in front of a fresh stave. Of course, with experience, I now know that what I was seeing was developers doing what developers do. Some were doing it well, and really understood the craft of development, while others were producing work that was less elegant and less well written. Over the years, I have been privileged enough to learn from some amazing developers but I keep coming back to the same basic question, time after time, namely…

How much better would I be as a developer now, if I could have listened to the thought processes of other developers when they were writing applications?

In this series of articles, I’m going to take you through what I’m thinking while I develop an application. The code that accompanies the article will be written as a “warts and all” development so you can see how I take something from the initial requirements phase through to something I would be happy for others to use. This means that the articles will show every mistake I make and the shortcuts I take while I’m fleshing ideas out. I’m not going to claim I’m a great developer here, but I am competent and experienced enough that this should help people who are new to the field get over their awe a lot earlier, and gain confidence in themselves.

Setting the Scene

In this article, I am going to tackle writing a Minimum Viable Product (MVP) for the GET API. When we create an MVP, we are providing the minimum code to perform a particular function. In other words, we are going to avoid adding too many features at this stage while we concentrate on building a solid foundation for our development. The GET API is a suitable place to start because, in my consideration, it is going to provide us with the basic functionality. One of the pitfalls of being an experienced developer is that you run into the danger of solutionizing something as soon as you start thinking about it, so the other advantage of picking a single area up is that it is going to help me focus on one small part of the system, and look at how I will build out that one part only.

Where to Find the Code

As this is a series of articles, with each article building on the previous, I am hosting the project at https://github.com/ohanlon/goldlight.xlcr. Each article will have its own version of the code in a branch so there will be an article1 branch for this article, article2 for the next one, and so on. The completed version of the code will be in master.

Starting the Code

When I created the two projects, two files were added; one inside my unit test project and one inside my class library. I’m going to remove the Class1.cs file from the class library but leave the unit test file. The approach I take to writing code using TDD always starts with a single test and no other code in place. With only the test file in place, I remove any temptation to shortcut this process.

Faced with a blank unit test, I have to make a decision about what I'm going to test. The first test I'm going to write tests that when I issue a get request to an endpoint, I get a response back. It's not going to say what the response looks like, simply that it's not null. So, why did I decide that this was what I wanted to start with? At this stage, all I have decided is that I want to execute a Get request. With this in mind, I have subconsciously reached two conclusions.

  1. I am going to name my class GetRequest. When I was defining what I was going to test, I decided that I was testing a Get request. When writing code, good naming is vital because it tells us what the purpose of the code is. This is an area that I often struggle with because I try many variations of names out before I settle on the one that fits the best. Sometimes, while I'm writing code, I'll simply call a class or method something like A until I've decided on the name that I want to use.
  2. In the process of deciding that I want to perform this request, I used the word execute. This seems like a good name for the method that will be used to send the request and get the response back, so Execute it is.

Checking whether an object is returned is a simple test so in the Test1 method that is present in our test file by default, let's write our test.

C#
[Fact]
public void Test1()
{
    GetRequest getRequest = new GetRequest();
    Assert.NotNull(getRequest.Execute("http://www.google.com"));
}

Right now, this code won't even build which is a failing form of a test in its own right. To fix this, I'm going to do the minimum to get this code to build. As I'm inside Visual Studio right now, I'm going to use it to help me so the first thing to do is to get Visual Studio to create a GetRequest class for me. If I click on the GetRequest text, I can press Alt and Enter to bring up Quick Actions. The first Quick Action allows me to generate a type. I could choose to create the class in its own file, as a nested class, or as a class in the same file. I'm going to create the class in a new file because the next step shows how Visual Studio can really help me here.

Quick Actions are features that Visual Studio provides whenever it thinks it can help you do something with your code (there is also a Ctrl and . operation that triggers Quick Actions).

So, I've now created the GetRequest class in the unit test project. Obviously, I don't want to leave this file here, I want it to be put in my class library instead. I'm going to drag the GetRequest.cs file from the unit test project into the class library, giving me two versions of the same file. I can now delete the GetRequest.cs file from the unit test project. If you're doing this exercise while reading this article, you'll probably not be surprised that the GetRequest type is showing as broken again. This is happening because the GetRequest class is internal; this didn't matter when it was in the same project but now that we've moved it to the class library, this means it's no longer visible. Let's change the scope of the GetRequest class to public.

C#
public class GetRequest
{
}

While I have made this class public, it is still in the wrong namespace. Right now, it is in the goldlight.xlcr.core.tests namespace. As I mentioned earlier, I need to fix the namespaces so that they follow Pascal naming conventions. I also need to move the get request class from a test namespace to a much more appropriate core one. I'm going to tackle the Pascal casing issue first.

In the class library, I'm going to add the RootNamespace element so my project file looks like this:

XML
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>Goldlight.Xlcr.Core</RootNamespace>
  </PropertyGroup>

</Project>

And in the unit test project, I'm going to add a RootNamespace of Goldlight.Xlcr.Core.Tests.

XML
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>Goldlight.Xlcr.Core.Tests</RootNamespace>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; 
       buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="1.3.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; 
       buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\goldlight.xlcr.core\goldlight.xlcr.core.csproj" />
  </ItemGroup>

</Project>

With the root namespaces fixed to be Pascal-cased instead of all lower case, I am ready to fix the problem with the GetRequest class being in a testing namespace. I could manually change the namespace to the correct one, but this means I have to add a using statement into the unit test file. Alternatively, I could let Visual Studio do the heavy lifting for me here and use a Quick Action to rename the namespace in the GetRequest file. This is the approach I prefer to take and comes in handy later on if I am going to be moving classes around in the solution. The fix is simple, on the namespace in GetRequest.cs, I bring up the Quick Actions (Alt + Enter) and I choose the option to Change namespace to Goldlight.Xlcr.Core. This action has the benefit of automatically adding the following line to the unit test file.

C#
using Goldlight.Xlcr.Core;

Back to the code and the unit test still won’t compile because GetRequest does not have an Execute method. I am going to fix that now using another Quick Action. I will click on the Execute name in the test and bring up the Quick Action to Generate the GetRequest.Execute method. This adds the following bare-bones method inside the GetRequest class.

C#
public object Execute(string v)
{
    throw new NotImplementedException();
}

I can now run my unit test. I know the test is going to fail because I am not returning anything, but the important thing is that I run the test right now.

You might be wondering why I write my tests this way. I’ve just spent a lot of time talking about something so there’s an implication that it takes a long time to write the test to create the class and the method. Surely my workflow should be to create the class and the method in the class library and then write the unit test for it. After all, my code won’t even compile until I have followed these steps. The reality is, this practice takes me a couple of seconds at most. By using Quick Actions, I let Visual Studio do the heavy lifting for me here (Rider also does this). When I get to the stage of adding extra methods to the class, this workflow becomes even more efficient.
Getting back to the code I’ve written, having run the test, I now have a failed test so I need to fix it. As I said earlier, following TDD means that I should fix it with the simplest code possible. Well, the simplest code I could write here is to return a new instance of some object from my method. What I’m going to do is change one keyword so instead of throwing a new NotImplementedException, I’m going to return it instead.

C#
public object Execute(string v)
{
    return new NotImplementedException();
}

I rerun my test and the test passes. I have successfully run my first passing test. One of the reasons I use TDD is because it helps me to focus more closely on what could break each piece of functionality. I no longer focus on just the positive cases. Instead, TDD helps me to get into the mindset of questioning what could break each public method I write.

Before we go any further, I need to consider whether the names I’m using in the code really convey what the tests are about and whether method parameter names are appropriate. I started, in this project, using the UnitTest1 class that the dotnet command had already created. This does not seem like a good name for a test class to me because it does not tell me what is being tested. Before I fix the name, I have just realized that I forgot to correct the name of the unit tests namespace when I set the RootNamespace earlier. I’m going to apply the same Quick Action "rename namespace" operation that I did earlier; by now, you will be familiar with how I do this, so I am not going to repeat it here.

Getting back to UnitTest1. Both the name of the file it’s in and the unit test class name are wrong. They need to be more meaningful, so I have to ask myself what a good name for a class containing tests for the GetRequest class should be. This seems to be a simple choice to me, I’m testing GetRequest so the test class will be called GetRequestTests (the Tests part because there will be multiple tests in here).

While the class is called GetRequestTests, the underlying file is still called UnitTest1.cs. If I click on the class name and select the Quick Actions operation, I have the ability to rename the file to match the class name now. This is a feature I tend to use fairly regularly as I often refine class names when I am giving them more context.
The last thing I need to consider is the fact that my sole test is in a method called Test1. Again, this does not provide any context for me when I see this test name in the test output window. If I am not intimately familiar with the tests, then Test1 is a completely useless name. What I need to do is add a name that tells me what the test is doing. To do this, I tend to write test names that follow the pattern:

C#
GivenSomeOperation_WhenSomeConditionsArePresent_ThenThisIsTheResultIExpect

Basically, this pattern is broken down into three parts that I can easily visually parse. The underscore separates the part of the sentence so that I can form a mental model of what is going on. If I apply this approach to my current test, then I am going to come up with a name such as:

C#
GivenCallToExecuteGetRequest_WhenTheEndpointIsSet_ThenNonNullResponseIsReturned

The beauty about a name like that is it forces me to start asking questions about the method I’m writing. The obvious question (well to me anyway) is what happens when I don’t set an endpoint? What response should I get? Initially, it seems as though I am presented with two choices here. I can either return an empty response because I am not requesting anything here or I can validate the input to see whether or not the input has been set and, if it has not, throw an exception.

In my mind, it makes more sense to prevent the operation from happening with an empty value than it does to return an empty response here. My thinking here is that throwing an exception here is telling the developers that the input state to this operation is invalid so further processing cannot possibly be performed on it and this is something I can easily pick up on elsewhere in the system when I’m actually calling this code. If I return an empty response, I am making an assumption that the only time I will get an empty response is if the input is empty. To be honest, at this stage, I still have questions about whether I should be returning anything from the Execute method at all. This is one of the problems when I jump headfirst into developing an application without considering the overall design. As I continue the development, I will be revisiting the issue of whether I should be returning the response or whether I should have the response as state inside GetRequest.

The last paragraph has convinced me that I should be throwing an exception if the endpoint is empty but I now have to decide what exception I want to raise. Choosing appropriate exceptions can seem to be a little bit of an art but it is vital to choose the most appropriate exception. What I am trying to guard against is having the string being either null or whitespace; something that is easy to test for with a standard string function. If I only had to consider that the string could be null, then throwing an ArgumentNullException would be appropriate. What I have to consider is the fact that a string could also be empty or whitespace so this is no longer just null. Now, I could treat all three as null for convenience but this does not seem right to me. Having ruled out ArgumentNullException, I am now in the position of considering that what I am really talking about here is that the argument may not be null but there is something else wrong with it. I could either create my own ArgumentNullOrWhitespaceException class or I can use a more general-purpose ArgumentException instance. At this stage, I am fine knowing that the problem is just that there is a problem with the endpoint so I will stick with the ArgumentException.

With this information in mind, I now know what my test needs to be:

C#
[Fact]
public void GivenCallToExecuteGetRequest_WhenNoEndpointIsSet_ThenArgumentExceptionIsThrown()
{
    GetRequest getRequest = new GetRequest();
    Assert.Throws<ArgumentException>(() => getRequest.Execute(""));
}

I am in a position to validate the input to my Execute method. Now, I’m only trying to fail one test condition at a time, so I am not going to change my Execute method to throw a new ArgumentException. If I did this, my other test would fail and I should be aiming to keep other tests stable while I fix the failures. The simplest fix here is to actually put the null or whitespace fix in place in the method (while I am at it, I am going to fix the poor choice of v for the parameter name).

C#
public object Execute(string endpoint)
{
    if (string.IsNullOrWhiteSpace(endpoint))
    {
        throw new ArgumentException();
    }
    return new NotImplementedException();
}

I am trying to decide whether or not this is the only check I can put in place in my Execute method. Am I only concerned whether or not this string is empty? I have to consider that the Execute method is expecting to receive an HTTP endpoint and I want it to be limited to either HTTP or HTTPS, so the start of the endpoint should actually start with HTTP or HTTPS. Armed with this knowledge, I can see that I have to write unit tests to verify that we will see an error if the endpoint does not start with either of these values. This really needs to be covered in more than one test (as a hint, we already have a suitable test for the HTTPS endpoint in place).
Going back to the decision-making process, trying to decide what type of exception we need to throw if the endpoint is not valid. I could, if I wanted, create a custom exception called something like EndpointInvalidException or I could raise the ArgumentException here. On one hand, EndpointInvalidException feels as though it could be really useful but, on the other hand, the ArgumentException is designed to say that there is a problem with the argument. It’s at this point that I realise that I have actually been looking at the wrong problem here and I have made a classic error of jumping into the code without thinking about my overall design.

The Case for Thinking Things Through

I started the "journey" with my code here, by jumping in and choosing a class to work on and start writing it. I even explained why I thought this was a suitable class to start with and that was okay. I am happy with those decisions; I even have the thought in the back of my mind that each operation is probably going to be represented as a Request class and they are probably going to share some commonality so there may well be an interface or abstract class relationship that we can establish as we refactor the code. However (okay, I know you shouldn't start a sentence with however, but it applies here), the reality is, that I jumped in a little too quickly. I have a Request class that expects an endpoint, so is it right that I want to put the endpoint in the Execute method? If I reuse the same Request instance for each operation, then I really should put the endpoint in the Execute method signature but if I have a new Request instance for each actual request, then I don't actually need the endpoint to be set in the Execute method. I could move the endpoint out of the Execute method altogether if I wanted.

Let us think through the implications of both approaches.

  • If I set up a single Request instance, then I should probably make the Request class a Singleton. If you aren't aware of what a Singleton is, it is a design pattern where we ensure that we can only ever have one instance of this class. There are many ways of doing this, whether it's through registering the Request class as a single instance in an IoC (Inversion of Control) container, or by managing the creation of the instance inside the Request itself. If you want more information about this, Jon Skeet has an excellent article here.
  • If I want to follow the single Request instance approach, then I have to consider that I am going to make it a lot harder to maintain any state that I may need. As I continue through this article series, you will see that this isn't necessarily the issue that it would seem but, for the moment I do have to consider whether or not I want to maintain state.
  • If I use a new instance per invocation, then there is a minor performance hit everytime I instantiate the Request class because I expect .NET to keep track of the lifespan for me, and there is a cost to creating the new instance. I can rule this out as a problem for my use case here because I am not concerned about wringing out the last drop of performance for my code here. In other words, I am not concerned about premature performance optimisation.
  • With a new instance, I get the chance to maintain state in a lot simpler fashion. There will be some state associated with the request so anything I can do to make this easier will obviously help.

Going back to basics, I have an endpoint here. In some respects, this endpoint is a specialization of the .NET Uri class. While the endpoint can be represented as a string, it is more than just a simple string. An endpoint is the combination of the address and the validation to prove that the endpoint is valid. In my current design, I am attempting to validate the endpoint inside a class that is intended to do something completely different. This tells me that I have violated Single Responsibility, so it is time for me to address this by creating my own Endpoint implementation (I am not going to use the standard .NET Uri class because I am deliberately constraining the endpoints to be either HTTP or HTTPS and the Uri class is much more open-ended than this).

The first stage of addressing this is to create an EndpointTest class inside my unit tests. I know that I am in danger of making this article very “press this key now”, so accept that the first test I am going to write follows the pattern from the last test classes where I create the test and generate the associated class and methods using Quick Actions.

Setting up the first test in my test class, I get the following code:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace Goldlight.Xlcr.Core.Tests
{
    public class EndpointTests
    {
        [Fact]
        public void GivenInstantiationOfAnEndpoint_WhenTheEndpointIsSet_ThenEndpointIsCreated()
        {
            Endpoint endpoint = new Endpoint("http://www.google.com");
            Assert.Equal(endpoint.Address, "http://www.google.com");
        }
    }
}

When I use the quick actions and perform similar actions to my earlier tests to move the code into the class library, I end up with this code:

C#
using System.Collections.Generic;

namespace Goldlight.Xlcr.Core
{
    public class Endpoint
    {
        private string v;

        public Endpoint(string v)
        {
            this.v = v;
        }

        public IEnumerable<char> Address { get; set; }
    }
}

Remember that TDD has us doing the quickest fix first to get the code to pass the test, I’m going to change the constructor to this:

C#
public Endpoint(string v)
{
    Address = "http://www.google.com";
}

As I am writing this article, it has become apparent that describing the TDD approach for every method is going to prove too tedious for someone reading it. I will now switch over to describing the code and the design decisions only; the unit tests can be viewed in the end solution.

Getting back to the Endpoint, I said that I wanted to validate the endpoint to ensure it starts with HTTP or HTTPS. While I was working on the tests for the Execute method, I was treating null/whitespace as a separate condition. While thinking about this, the only time an endpoint is valid is if it starts (or we can imply the start) with HTTP or HTTPS. Whether it is null, whitespace or a relative URI is immaterial. Anything other than the http: or https: schemes should throw an ArgumentException.

The tests that I will be adding to cover these cases are:

C#
[Fact]
public void GivenInstantiationOfEndpoint_WhenTheEndpointIsNull_ThenArgumentExceptionIsThrown()
{
    Assert.Throws<ArgumentException>(() => new Endpoint(null));
}

[Fact]
public void 
GivenInstantiationOfEndpoint_WhenTheEndpointIsHttpWithoutTheColon_ThenArgumentExceptionIsThrown()
{
    Assert.Throws<ArgumentException>(() => new Endpoint("httpwww.google.com"));
}

[Fact]
public void 
GivenInstantiationOfEndpoint_WhenTheEndpointIsHttpsWithoutTheColon_ThenArgumentExceptionIsThrown()
{
    Assert.Throws<ArgumentException>(() => new Endpoint("httpswww.google.com"));
}
[Fact]
public void 
GivenInstantiationOfEndpoint_WhenTheEndpointHasLeadingSpaces_ThenTheEndpointIsAccepted()
{
    Endpoint endpoint = new Endpoint(" HTTPs://www.google.com");
    Assert.Equal(endpoint.Address, " HTTPs://www.google.com");
}
[Fact]
public void 
GivenInstantiationOfEndpoint_WhenTheEndpointIsCaseInsensitive_ThenTheEndpointIsAccepted()
{
    Endpoint endpoint = new Endpoint("HTTP://www.google.com");
    Assert.Equal(endpoint.Address, "HTTP://www.google.com");
}

It may come as a surprise but validating URIs can lead to a series of complicated tests if I was going to try and roll my own regular expression, for instance. I could perform simple string manipulation to get the start – for example, I could split the string based on the colon and then check to see that the first part is either of our conditions. While this is perfectly doable, the reality is, that we are working with pattern matching here so it makes sense to use the tooling that already does the heavy lifting on this task. Namely, I am going to use the standard .NET URI implementation. Specifically, I am going to see if I can use TryCreate to create a URI that maps onto the endpoint, checking to make sure the scheme is HTTP or HTTPS.

With this Uri validation check in mind, I’m also going to do a little bit of refactoring inside the Endpoint class to make the Address value return a string and it will be assigned directly from the constructor so the private variable can be removed. With the check added to the constructor parameter check, our code now looks like this:

C#
public class Endpoint
{
    public Endpoint(string endpoint)
    {
        if (string.IsNullOrWhiteSpace(endpoint) || !IsHttpFormatEndpoint(endpoint))
        {
            throw new ArgumentException(null, nameof(endpoint));
        }
        Address = endpoint;
    }

    public string Address { get; }

    private bool IsHttpFormatEndpoint(string endpoint)
    {
        return Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uriResult)
          && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
    }
}

Something you will find as I continue writing, I favour small classes so this is just about the right length for me and this really fits as a class that's close to following Single Responsibility. The functionality is neat and self-contained and decouples the knowledge of how to create valid endpoints away from anything that actually uses it. The reason I say that the class is close to following Single Responsibility is because I have mixed a simple bit of validation into the class. Strictly speaking, I should be separating the validation of the endpoint into its own class but I have decided to be pragmatic here and mix this in here. As I progress with the codebase, I may revisit this decision and separate the validation out but, right now, I'm happy to leave this where it is.

I have an Endpoint class in place so I am ready to revisit the GetRequest class to use this. I have some refactoring to do to use the Endpoint rather than passing in a string into the Execute method. I could instantiate the class with an Endpoint but I am going to keep things simple right now and change the Execute method so that instantiates an Endpoint instance using the value passed in. As I have unit tests already in place, I should be able to verify that the change works as expected and the GetRequest class now looks like this:

C#
using System;

namespace Goldlight.Xlcr.Core
{
    public class GetRequest
    {
        public object Execute(string endpoint)
        {
            Endpoint uriEndoint = new Endpoint(endpoint);
            return new NotImplementedException();
        }
    }
}

Adding Query String Parameters

Now that I have a basic GetRequest class with a nice clean Execute method, I want to turn my attention to how I want to handle features such as having query string parameters. One way that I could do this is to force the user to just pass in these values as part of the endpoint so a query like https://peters.dummyapi.com/get/{id} would be passed in as https://peters.dummyapi.com/get/1. While this is certainly an option, I would prefer to provide the ability to use substitution parameters in the API name. Before I start sketching out the code, I'm going to break down what I am going to try and achieve with this.

The first thing is that we should allow any number of substitution parameters in this API. The substitutions consist of a key and a value so, in our example above, the key is id, and the value is 1. If my query string list has a key, then the key must have a corresponding value like this.

Key Value  
id   This is not allowed
id 1 This is allowed

As I know that we want to allow multiple key/value pairs, my thought here is that I want to use a Dictionary to manage this. What I need to decide is what types I want to use in the key/value pairs. While the example for the id has the value as a number, we could have any number of different values here so I am going to choose to use a string key and a string value.

I could choose to put the Dictionary directly into the GetRequest class but what I would rather do is create a separate class to manage the query string values as the thought that is going through my head right now is that we will pass the endpoint through the query string class to get the transformed endpoint.

With this initial design in mind, I'm ready to start the implementation off. I'm going to be creating a QueryString class that encapsulates the ability to add query string parameters so I will write an Add method that guards against empty keys and values. I could have avoided putting the guard in for the key and let the underlying dictionary throw an error when I attempted to create an entry with an invalid key but, by wrapping this up I am going to control the message that comes back into something I could use in the UI when I get around to writing it. The Add method is going to look like this:

C#
public void Add(string key, string value)
{
  if (string.IsNullOrWhiteSpace(key)) 
      throw new ArgumentException("You must supply a key for this query string parameter.");
  if (string.IsNullOrWhiteSpace(value)) 
      throw new ArgumentException("You must supply a value for this query string parameter.");

  _queryStringParameters[key] = value;
}

While I've been thinking about the code, there has been a question niggling away at the back of my mind; namely, how is this going to interact with the endpoint class I wrote before? Am I going to transform the address first, and then pass it to the endpoint? Am I going to take an endpoint instance and transform the address? How am I going to hook this up in my GetRequest class?

If I put the QueryString transformation inside the Endpoint class then, with the current architecture I have, I would have to pass the QueryString instance in, as well as the url. If I keep the QueryString inside the GetRequest.Execute method, then when I want to add PostRequest, PatchRequest, and so on, implementations, I am going to have to apply the same QueryString application in each instance. There are pros and cons for each of the different implementations. Right now, the solution would appear to be to encapsulate the QueryString inside the Endpoint class so that we internalise the transformation of the address.

If we move the QueryString class inside the Endpoint, then we are going to need to update the constructor to accept it. I know that the users may not always supply a query strings so if this parameter is null, I'm going to use an empty instance of it. When the transformation of the URL is applied, the empty instance will just return a copy of the original URL.

Let's see what changes we want to make to our Endpoint constructor. We are going to change it from this:

C#
public Endpoint(string endpoint)
{
  if (!IsHttpFormatEndpoint(endpoint))
  {
    throw new ArgumentException(null, nameof(endpoint));
  }
  Address = endpoint;
}

To this:

C#
public Endpoint(string endpoint, QueryString queryString)
{
  if (!IsHttpFormatEndpoint(endpoint))
  {
    throw new ArgumentException(null, nameof(endpoint));
  }
  Address = endpoint;
}

Obviously, this is not going to transform an endpoint at this point, so I am going to add this feature to our QueryString class. Again, following TDD principles, I am going to solve the simplest problem first and add a Transform method that simply returns the string that I pass in. After I write my Transform method, my Endpoint constructor looks like this:

C#
public Endpoint(string endpoint, QueryString queryString = null)
{
  if (queryString != null)
  {
    endpoint = queryString.Transform(endpoint);
  }
  if (!IsHttpFormatEndpoint(endpoint))
  {
    throw new ArgumentException(null, nameof(endpoint));
  }

  Address = endpoint;
}

Note that there were many different designs I could have chosen to link the Endpoint and the QueryString handling together. As this is an MVP, I have chosen to adopt the simplest approach possible. By applying a default of null as the QueryString parameter in the constructor, I have minimised the places I needed to touch to fix the code.

Transforming the Query String

The problem with the code that we have so far is that it doesn't actually transform the URL right now. I have not finished writing the transformation code and it's time for me to do that. The approach I am going to take is simply to iterate over the key/value pairs in the dictionary and use string.Replace to replace the parameter with the value. I have decided that the URL that the user will supply will use moustache brackets {} to denote values that can be replaced, so an API like https://www.dummyapi.com/get/{id} would have a key of id, with the value being set to an appropriate choice.

Applying this logic, our Transform can be changed to look like this:

C#
public string Transform(string url)
{
  foreach (KeyValuePair<string, string> parameter in _queryStringParameters)
  {
    url = url.Replace($"{{{parameter.Key}}}", parameter.Value);
  }
  return url;
}

For cases where the keys match, this code is enough but I have to ask what happens if the key is id in the URL, but the key is ID in my QueryString? I am simply going to add StringComparison.OrdinalIgnoreCase as a parameter in the Replace method. It is important to remember that as a string is an immutable object, each call through Replace is creating a new instance in memory. I have chosen to accept this as a suitable tradeoff in terms of performance, for the speed of development.

Right now, I am going to leave this code exactly as it is, content in the knowledge that if we don't have a matching key to entry in the URL, then the Endpoint class will flag up that the URL is not valid. This helps me to limit the code to the bare minimum that achieves our MVP.

Conclusion

In this article, I have introduced the basic implementation of Postman like functionality for a GET method. The code does not currently call out to any actual endpoint but we have seen an introduction to TDD and a look at why I have made the decisions I have made. In the next article, we are going to wrap up the first part of our GET functionality by adding request headers support and demonstrating how to actually call out to HTTP. I will also demonstrate how I go about testing HTTP calls without needing physical endpoints.

History

  • 12th May, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)