It is a simple rock, paper, and scissors game used to study how to build a full RESTful API. You will be able to find the latest code in this repo. BTW, if you like it, give it a star.
To make it fun, I also released a live client at https://roshambo.codewithsaar.com.
For any response, I intend to return what are the possible actions in a collection named next
. That brings the API closer to fitting into RESTful constraints of the response that contains hypermedia.
For example, the very first GET
at /
(try it here), this is an example of the response:
{
"suggestedUserId": {
"value": "2883385a-7af4-4f41-a69e-640ceee50053"
},
"self": {
"href": "https://roshamboapp.azurewebsites.net/",
"rel": "self",
"method": "get",
"key": null
},
"next": [
{
"href": "https://roshamboapp.azurewebsites.net/players/{uid}",
"rel": "ready",
"method": "get",
}
]
}
Pay attention to the next
object, there is one action, and it has 3 properties: href
, rel
, and method
, and in the model, I used a class to represent it:
public class RelModel
{
public string Rel { get; init; } = default!;
public string Href { get; }
public string Method { get; init; } = HttpMethod.Get.ToString().ToLowerInvariant();
}
It is pretty standard. Now, follow the link to invoke the 2nd GET
, (try it here)
The response looks like this:
{
"next": [
{
"href": "https://roshamboapp.azurewebsites.net/rounds/rock",
"rel": "ready",
"method": "post",
"key": "rock"
},
{
"href": "https://roshamboapp.azurewebsites.net/rounds/paper",
"rel": "ready",
"method": "post",
"key": "paper"
},
{
"href": "https://roshamboapp.azurewebsites.net/rounds/scissor",
"rel": "ready",
"method": "post",
"key": "scissor"
}
],
...
}
I have 3 actions in the next
collection, and there is an additional key
property for each action. To represent that, I introduced 2 levels of classes:
public abstract class RelModelWithKey : RelModel // Abstraction of the action with key
{
public abstract string Key { get; }
}
public class RockAciton : RelModelWithKey // Concrete action of rock
{
...
}
public class PaperAction : RelModelWithKey // Concrete action of paper
...
public class ScissorAction : RelModelWithKey // Concrete action of scissor
Now, let's abstract a class for the result. Since I want all the responses to always return an action list, I am willing to introduce a base class:
public class ResponseBase
{
public RelModel Self { get; init; }
public IEnumerable<RelModel> Next { get; init; }
}
Here's a problem: in .NET 6 or prior, any action in Next
will be serialized using the base class, RelModel
in this case, that it won't have Key
property for any instance of RelModelWithKey
. Probably a way to work around it is to write a custom serializer.
In .NET 7, the serializer will do a type check for the derived class - as long as it is in the list like this:
[JsonDerivedType(typeof(RockAction))]
[JsonDerivedType(typeof(PaperAction))]
[JsonDerivedType(typeof(ScissorAction))]
[JsonDerivedType(typeof(CustomRel))]
public class RelModel
{
...
}
Upon serialization, the serializer will check whether the runtime object matches any known type by JsonDerivedType
attribute. If it matches, it will then serialize the object with the matched type than the general base type.
Not only that addressed my problem, it also enables us to put any derived class of RelModel
into the Next
collection, as long as we mark the derived class with JsonDerivedType
attribute!
I think the polymorphic serialization capability makes life easier!