Creating Your First C# Application

Let’s use the JSONPlaceholder module we prepared to create our first C# application. This chapter assumes that the reader is familiar with C#.

Setting up the Project

In Visual Studio create JsonPlaceholder C# project (of any type you prefer) and move json_placeholder.igor into the project folder.

Use NuGet to install Artplant.Json package, and also reference Igor.dll, which is required for Igor runtime.

Enable C# HTTP client generation

Edit the json_placeholder.igor file and put the [csharp http.client] attribute before JSONPlaceholderService declatation:

[csharp http.client]
webservice JSONPlaceholderService

This attribute prescribes C# code generator to generate code for C# web client. For example, you may want to generate C# client and Erlang server, then you also have to add [erlang http.server] attribute. csharp attributes are ignored when generating Erlang code and vice versa.

Generating Code

To generate C# code run the folloding command (adjust igorc.exe path or put igorc.exe to the same folder):

igorc.exe -cs json_placeholder.igor

-cs attribute tells igorc.exe to generate the C# code. After command execution you will get the file with the following or similar content:

// Author: Igor compiler
// Compiler version: igorc 1.6.0
// DO NOT EDIT THIS FILE - it is machine generated

using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Igor.Web;

using JsonSerializer = Json.Serialization.JsonSerializer;

namespace JsonPlaceholder
{
   public sealed class Post
   {
      public int UserId { get; set; }
      public int Id { get; set; }
      public string Title { get; set; }
      public string Body { get; set; }

      public Post()
      {
      }
   }

   public sealed class PostJsonSerializer : Json.Serialization.IJsonSerializer<Post>
   {
      public static readonly PostJsonSerializer Instance = new PostJsonSerializer();

      public Json.ImmutableJson Serialize(Post value)
      {
            if (value.Title == null)
               throw new System.InvalidOperationException("Required property Title is null");
            if (value.Body == null)
               throw new System.InvalidOperationException("Required property Body is null");
            var json = new Json.JsonObject();
            json["userId"] = JsonSerializer.Int.Serialize(value.UserId);
            json["id"] = JsonSerializer.Int.Serialize(value.Id);
            json["title"] = JsonSerializer.String.Serialize(value.Title);
            json["body"] = JsonSerializer.String.Serialize(value.Body);
            return json;
      }

      public Post Deserialize(Json.ImmutableJson json)
      {
            var result = new Post();
            Deserialize(json.AsObject, result);
            return result;
      }

      public void Deserialize(Json.ImmutableJsonObject json, Post value)
      {
            value.UserId = JsonSerializer.Int.Deserialize(json["userId"]);
            value.Id = JsonSerializer.Int.Deserialize(json["id"]);
            value.Title = JsonSerializer.String.Deserialize(json["title"]);
            value.Body = JsonSerializer.String.Deserialize(json["body"]);
      }
   }

   public class JsonPlaceholderService : System.IDisposable
   {
      protected HttpClient HttpClient { get; }

      public JsonPlaceholderService(HttpClient httpClient)
      {
            HttpClient = httpClient;
      }

      public void Dispose()
      {
            HttpClient.Dispose();
      }

      public async Task<List<Post>> GetPostsAsync(int? userId = null, CancellationToken cancellationToken = default(CancellationToken))
      {
            var queryBuilder = new WebQueryBuilder("posts");
            if (userId != null)
               UriFormatter.Int.AppendQueryParameter(queryBuilder, "userId", userId);
            using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, queryBuilder.ToString()))
            using (var httpResponse = await HttpClient.SendAsync(httpRequest, cancellationToken))
            {
               httpResponse.EnsureSuccessStatusCode();
               using (var responseStream = await httpResponse.Content.ReadAsStreamAsync())
               using (var reader = new StreamReader(responseStream))
                  return JsonSerializer.List(PostJsonSerializer.Instance).Deserialize(Json.JsonParser.Parse(reader));
            }
      }

      public async Task<Post> GetPostAsync(int id, CancellationToken cancellationToken = default(CancellationToken))
      {
            using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, $"posts/{id}"))
            using (var httpResponse = await HttpClient.SendAsync(httpRequest, cancellationToken))
            {
               httpResponse.EnsureSuccessStatusCode();
               using (var responseStream = await httpResponse.Content.ReadAsStreamAsync())
               using (var reader = new StreamReader(responseStream))
                  return PostJsonSerializer.Instance.Deserialize(Json.JsonParser.Parse(reader));
            }
      }

      public async Task<Post> PutPostAsync(Post requestContent, int id, CancellationToken cancellationToken = default(CancellationToken))
      {
            if (requestContent == null)
               throw new System.ArgumentNullException("requestContent");
            using (var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"posts/{id}"))
            {
               httpRequest.Content = new StringContent(PostJsonSerializer.Instance.Serialize(requestContent).ToString(), System.Text.Encoding.UTF8, "application/json");
               using (var httpResponse = await HttpClient.SendAsync(httpRequest, cancellationToken))
               {
                  httpResponse.EnsureSuccessStatusCode();
                  using (var responseStream = await httpResponse.Content.ReadAsStreamAsync())
                  using (var reader = new StreamReader(responseStream))
                        return PostJsonSerializer.Instance.Deserialize(Json.JsonParser.Parse(reader));
               }
            }
      }
   }
}

As you can see, the root namespace is JsonPlaceholder - it is the module name spelled according to C# code guidelines. Namespace contains three declarations:

  1. Post class, which corresponds to Post record from Igor file.

  2. PostJsonSerializer class that can serialize instances of Post class to JSON (and deserialize it from JSON).

  3. JsonPlaceholderService which provides webservice API that we defined in Igor file.

At this point, the project should compile successfully but do nothing.

Using Generated Code

Let’s write some tests to unsure that our API works. Use NuGet to add NUnit to the project, and create TextFixture:

using System;
using System.Net.Http;
using System.Threading.Tasks;
using NUnit.Framework;

namespace JsonPlaceholder.Test
{
    [TestFixture]
    class JsonPlaceholderTests
    {
        JsonPlaceholderService api = new JsonPlaceholderService(new HttpClient { BaseAddress = new Uri("https://jsonplaceholder.typicode.com") });

        [Test]
        public async Task GetPostsTest()
        {
            var posts = await api.GetPostsAsync();
            Assert.AreEqual(100, posts.Count);
        }

        [Test]
        public async Task GetPostsByUserTest()
        {
            var allPosts = await api.GetPostsAsync();
            var userId = allPosts[13].UserId;

            var posts = await api.GetPostsAsync(userId: userId);
            foreach (var post in posts)
                Assert.AreEqual(userId, post.UserId);
        }
    }
}

The code above contains two tests for GetPosts resource. GetPostsTest gets all posts and checks that their count is 100 (which should be true by JSONPlaceholder service design), and GetPostsByUserTest gets a post by a random user id and checks that only posts by that user are returned in response.

You can run tests to ensure that they pass.

As you can see, usage of the generated API is simple and straightforward.

Generating Equals function

Let’s add one more test for a GetPost resource:

[Test]
public async Task GetPostTest()
{
    var allPosts = await api.GetPostsAsync();
    var post = allPosts[13];

    var gotPost = await api.GetPostAsync(post.Id);
    Assert.AreEqual(post, gotPost);
}

Here we get an aribtrary post from the full list of all posts, fetch it by id and compare it with the original one.

If you run this test, it will fail, cause there’s no Equals function defined for Post class (and reference equality obviously fails).

Fortunately, Igor can help to fix the problem. Add the equals attribute to Post record declaration and regenerate the C# code:

[* json.enabled]
[csharp equals]
record Post
{
   int userId;
   int id;
   string title;
   string body;
}
igorc.exe -cs json_placeholder.igor

Here is how the generated Post class looks now:

public sealed class Post : System.IEquatable<Post>
{
    public int UserId { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }

    public Post()
    {
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;
            hash = hash * 23 + UserId.GetHashCode();
            hash = hash * 23 + Id.GetHashCode();
            hash = hash * 23 + (Title == null ? 0 : Title.GetHashCode());
            hash = hash * 23 + (Body == null ? 0 : Body.GetHashCode());
            return hash;
        }
    }

    public override bool Equals(object other)
    {
        return Equals(other as Post);
    }

    public bool Equals(Post other)
    {
        if (object.ReferenceEquals(other, null))
            return false;
        if (object.ReferenceEquals(this, other))
            return true;
        return UserId == other.UserId && Id == other.Id && Title == other.Title && Body == other.Body;
    }
}

As you can see, Igor has generated GetHashCode and Equals functions for you.

If you run the GetPostTest now, you can see that it succeeds.

It’s up to the reader to create a test for PutPost resource.