Building RPC-style web APIs

The RPC-style is based on the Remote Procedure Call paradigms, which have existed for a long time now (since the early 1980s). It is based on including an action name in the URL, which makes it very similar to standard MVC actions.

One of the big advantages of ASP.NET Core 3 is that you do not need to separate the MVC parts from the web API parts. Instead, you can use both in your controller implementations.

Controllers are now capable of rendering view results, as well as JSON/XML API responses, which enables easy migrations from one to the other. Additionally, you can use a specific route path or the same route path for your MVC actions.

In the following example, you are going to transform a controller action from an MVC view result into an RPC-style web API:

  1. Add a new method called ConfirmEmail to UserRegistrationController; it will be used to confirm the user registration email. The method accepts an email as a parameter, gets the user by the supplied email, and if the user is found, it updates the fact that the user has had their email confirmed and sets the timestamp of when it was confirmed:

        [HttpGet] 
        public async Task<IActionResult> ConfirmEmail(string email) 
        { 
          var user = await _userService.GetUserByEmail(email); 
          if (user != null) 
          { 
            user.IsEmailConfirmed = true; 
            user.EmailConfirmationDate = DateTime.Now; 
            await _userService.UpdateUser(user); 
            return RedirectToAction("Index", "Home"); 
          } 
          return BadRequest(); 
        } 

  1. Update the ConfirmGameInvitation method within GameInvitationController, store the email of the invited user in a session variable, and register the new user via the user service:
        [HttpGet] 
        public async Task<IActionResult> ConfirmGameInvitation
(Guid id,
[FromServices]IGameInvitationService
gameInvitationService) { var gameInvitation = await gameInvitationService.Get(id); gameInvitation.IsConfirmed = true; gameInvitation.ConfirmationDate = DateTime.Now; await gameInvitationService.Update(gameInvitation); Request.HttpContext.Session.SetString("email",
gameInvitation.EmailTo); await _userService.RegisterUser(new UserModel { Email = gameInvitation.EmailTo, EmailConfirmationDate =
DateTime.Now, IsEmailConfirmed =true }); return RedirectToAction("Index", "GameSession", new { id }); }
  1. Update the table element in GameSessionViewComponent, which can be found inside the Views/Shared/Components/GameSession/default.cshtml file, by removing the @if (Model.ActiveUser?.Email == email) wrap. Next, instead of wrapping the table element with a gameBoard div element (as shown in the following code), update the wait turn div element, which has an id called "divAlertWaitTurn", as follows:
 <div id="gameBoard">
<table>
...
</table>
</div>
<div class="alert" id="divAlertWaitTurn">
<i class="glyphicon glyphicon-alert">Please wait until
the other user has finished his turn.</i>
</div>
  1. Add a new JavaScript file within the wwwrootappjs folder called GameSession.js. This will be used to call the web API. The SetGameSession method accepts a session ID, which is used for the setting the game session: 
function SetGameSession(gdSessionId, strEmail) { 
          window.GameSessionId = gdSessionId; 
          window.EmailPlayer = strEmail; 
        } 
 
        $(document).ready(function () { 
          $(".btn-SetPosition").click(function () { 
            var intX = $(this).attr("data-X"); 
            var intY = $(this).attr("data-Y"); 
            SendPosition(window.GameSessionId, window.EmailPlayer,
intX, intY); }) })

Then, send the position, as follows:

function SendPosition(gdSession, strEmail, intX, intY) { 
          var port = document.location.port ? (":" +
document.location.port) : ""; var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restApi/v1/SetGamePosition/" + gdSession; var obj = { "Email": strEmail, "x": intX, "y": intY };

Add a temporary alert box for testing purposes:

         
          var json = JSON.stringify(obj); 
          $.ajax({ 
            'url': url, 
            'accepts': "application/json; charset=utf-8", 
            'contentType': "application/json", 
            'data': json, 
            'dataType': "json", 
            'type': "POST", 
            'success': function (data) { 
              alert(data); 
            } 
          }); 
        } 
  1. Add the preceding JavaScript file to the bundleconfig.json file so that you can bundle it with the other files into the site.js file:
        { 
          "outputFileName": "wwwroot/js/site.js", 
          "inputFiles": [ 
            "wwwroot/app/js/scripts1.js", 
            "wwwroot/app/js/scripts2.js", 
            "wwwroot/app/js/GameSession.js" 
          ], 
          "sourceMap": true, 
          "includeInProject": true 
        }, 
  1. Add a new property called Email to the TurnModel model:
        public string Email { get; set; } 
  1. Update the SetPosition method within GameSessionController. Here, expose it as a web API so that you can receive AJAX calls from the JavaScript SendPosition function we implemented previously:
[Produces("application/json")]
[HttpPost("/restapi/v1/SetGamePosition/{sessionId}")]
public async Task<IActionResult> SetPosition([FromRoute]Guid sessionId)
{
if (sessionId != Guid.Empty)
{
using (var reader = new StreamReader(Request.Body,
Encoding.UTF8, true, 1024, true))
{
...
}
}
return BadRequest("Id is empty");
}

Then, add the following code to the StreamReader body:  


var bodyString = reader.ReadToEnd();
if (string.IsNullOrEmpty(bodyString))
return BadRequest("Body is empty");
var turn = JsonConvert.DeserializeObject<TurnModel>(bodyString);
turn.User = await HttpContext.RequestServices.
xGetService<IUserService>().GetUserByEmail(turn.Email);
turn.UserId = turn.User.Id;
if (turn == null) return BadRequest("You must pass a TurnModel
object in your body");
var gameSession = await _gameSessionService.
GetGameSession(sessionId);
if (gameSession == null)
return BadRequest($"Cannot find Game Session {sessionId}");
if (gameSession.ActiveUser.Email != turn.User.Email)
return BadRequest($"{turn.User.Email} cannot play this turn");
gameSession = await _gameSessionService.
AddTurn(gameSession.Id, turn.User.Email, turn.X, turn.Y);
if (gameSession != null && gameSession.ActiveUser.Email !=
turn.User.Email)
return Ok(gameSession);
else
return BadRequest("Cannot save turn");
Note that it is good practice to prefix web APIs with a meaningful name and a version number (for example, /restapi/v1), as well as support for JSON and XML.
  1. Update the Game Session Index View in the Views folder and call the JavaScript SetGameSession function with the corresponding parameters:
        @using Microsoft.AspNetCore.Http 
        @model TicTacToe.Models.GameSessionModel 
        @{ 
          var email = Context.Session.GetString("email"); 
        } 
        @section Desktop { 
          ...
        } 
        @section Mobile{ 
          ...
        } 
        <h3>User Email @email</h3> 
        <h3>Active User <span id="activeUser">
@Model.ActiveUser?.Email</span></h3>
<vc:game-session game-session-id="@Model.Id"></vc:game-
session> @section Scripts{ <script> SetGameSession("@Model.Id", "@email");
</script> }
  1. Update the ProcessEmailConfirmation method for WebSockets in the communication middleware:
        public async Task ProcessEmailConfirmation(HttpContext 
context,
WebSocket currentSocket, CancellationToken ct, string
email) { var user = await _userService.GetUserByEmail(email); while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue &&
user?.IsEmailConfirmed == false) { await SendStringAsync(currentSocket,
"WaitEmailConfirmation", ct); await Task.Delay(500); user = await _userService.GetUserByEmail(email); } if (user.IsEmailConfirmed) await SendStringAsync(currentSocket, "OK", ct); }
  1. Update the ProcessGameInvitationConfirmation method for WebSockets in the communication middleware:
        public async Task ProcessEmailConfirmation(HttpContext 
context, WebSocket currentSocket, CancellationToken ct,
string email)
{
var user = await _userService.GetUserByEmail(email);
while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue && user?
.IsEmailConfirmed == false)
{
await SendStringAsync(currentSocket,
"WaitEmailConfirmation", ct);
await Task.Delay(500);
user = await _userService.GetUserByEmail(email);
}

if (user.IsEmailConfirmed)
await SendStringAsync(currentSocket, "OK", ct);
}

  1. Update the CheckGameInvitationConfirmationStatus method in the scripts2.js JavaScript file. It has to verify the returned data:
        function CheckGameInvitationConfirmationStatus(id) { 
          $.get("/GameInvitationConfirmation?id=" + id, function 
(data) { if (data.result === "OK") { if (interval !== null) { clearInterval(interval); } window.location.href = "/GameSession/Index/" + id; } }); }
  1. Update the Process method in the Gravatar Tag Helper and handle the case where no photo exists correctly:
        public override void Process(TagHelperContext context, 
TagHelperOutput output)
{
byte[] photo = null;
if (CheckIsConnected()) photo = GetPhoto(Email);
else
{
string filePath = Path.Combine(Directory.
GetCurrentDirectory(),"wwwroot", "images",
"no-photo.jpg");
if (File.Exists(filePath)) photo =
File.ReadAllBytes(filePath);
}
if (photo != null && photo.Length > 0)
{
output.TagName = "img";
output.Attributes.SetAttribute("src",
$"data:image/jpeg;base64,
{Convert.ToBase64String(photo)}");
}
}
  1. Update the Add method in GameInvitationService:
        public Task<GameInvitationModel> Add(GameInvitation
Model gameInvitationModel) { _gameInvitations.Add(gameInvitationModel); return Task.FromResult(gameInvitationModel); }

  1. Update the Desktop Layout Page and Mobile Layout Page. Clean this up by removing the development environment tag containing script1.js and script2.js at the bottom of both pages.
  2. Update the scripts1.js JavaScript file and clean up the previous unnecessary code by removing all the alert boxes that display whether WebSockets are enabled.
  1. Start the application, register a new user, start a game session by inviting another user, and click on a cell. Now, you will see a JavaScript alert box:

So far, you have learned how to transform the existing GameSessionController action into an RPC-style web API. Since all the different ASP.NET web frameworks have been centralized into a single framework in ASP.NET Core 3, this can be done easily and quickly without rewriting any code or changing your existing code too much.

In the next step, we will learn how to add a new method to the RPC-style web API to check if the turn for the current user has finished, which means that the next user can start their turn:

  1. Add a new property called TurnNumber to GameSessionModel in order to track the current turn number:
        public int TurnNumber { get; set; } 
  1. Add a new property called IconNumber to TurnModel so that you can define what icon (X or O) needs to be used for display later:
        public string IconNumber { get; set; } 
  1. Add a new method called GetGameSession, which uses the game session service to get a game session, to the GameSessionController; it will be exclusive to web API calls:
        [Produces("application/json")]
[HttpGet("/restapi/v1/GetGameSession/{sessionId}")]
public async Task<IActionResult> GetGameSession(Guid
sessionId)
{
if (sessionId != Guid.Empty)
{
var session = await _gameSessionService.
GetGameSession(sessionId);

if (session != null)
return Ok(session);
else
return NotFound($"cannot found session
{sessionId}");
}
else
return BadRequest("session id is null");
}

  1. Update the AddTurn method in GameSessionService so that it calculates the IconNumber and TurnNumber. To do this, replace the following line of code:
turns.Add(new TurnModel {
User = await _UserService.GetUserByEmail(email), X = x,
Y = y });

Write the following code, which allows an icon number to be set: 

public async Task<GameSessionModel> AddTurn(Guid id,
string email, int x, int y) { ... turns.Add(new TurnModel { User = await _UserService.GetUserByEmail(email), X = x, Y = y, IconNumber = email == gameSession.User1?.
Email ? "1" : "2" }); gameSession.Turns = turns; gameSession.TurnNumber = gameSession.TurnNumber + 1;
... }
  1. Update the Game Session Index View, user images, and add the possibility to enable and disable the gameboard by replacing the scripts section at the bottom with the following code snippet. This enables or disables the board, depending on whether a user is active or not:
        @section Scripts{ 
          <script> 
          SetGameSession("@Model.Id", "@email"); 
          EnableCheckTurnIsFinished(); 
          @if(email != Model.ActiveUser?.Email) 
          { 
            <text>DisableBoard(@Model.TurnNumber);</text> 
          } 
          else 
          { 
            <text>EnableBoard(@Model.TurnNumber);</text> 
          } 
          </script> 
        } 

  1. Add a new JavaScript file called CheckTurnIsFinished.js to the wwwrootappjs folder using the following EnableCheckTurnIsFinished() function. This checks whether a playing turn has finished:
function EnableCheckTurnIsFinished() { 
          interval = setInterval(() => {CheckTurnIsFinished();}, 
2000); } function CheckTurnIsFinished() { var port = document.location.port ? (":" +
document.location.port) : ""; var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restapi/v1/GetGameSession/" + window.GameSessionId; $.get(url, function (data) { if (data.turnFinished === true && data.turnNumber >=
window.TurnNumber) { CheckGameSessionIsFinished(); ChangeTurn(data); } }); }

In the same CheckTurnIsFinished.js file, add a ChangeTurn() function. This changes the turn of a player and disables or enables the board accordingly: 

function ChangeTurn(data) { 
          var turn = data.turns[data.turnNumber-1]; 
          DisplayImageTurn(turn); 
 
          $("#activeUser").text(data.activeUser.email); 
          if (data.activeUser.email !== window.EmailPlayer) { 
            DisableBoard(data.turnNumber); 
          }  
          else { 
            EnableBoard(data.turnNumber); 
          } 
        } 

Add the actual functionality to disable and enable the board, as follows: 

       function DisableBoard(turnNumber) { 
          var divBoard = $("#gameBoard"); 
          divBoard.hide(); 
          $("#divAlertWaitTurn").show(); 
          window.TurnNumber = turnNumber; 
        } 
 
        function EnableBoard(turnNumber) { 
          var divBoard = $("#gameBoard"); 
          divBoard.show(); 
          $("#divAlertWaitTurn").hide(); 
          window.TurnNumber = turnNumber; 
        } 
 

Finally, add a DisplayImageTurn function, which manipulates the cascading style sheets according to a respective turn, as follows: 

function DisplayImageTurn(turn) { 
          var c = $("#c_" + turn.y + "_" + turn.x); 
          var css; 
 
          if (turn.iconNumber === "1") { 
          css = 'glyphicon glyphicon-unchecked'; 
        } 
        else { 
          css = 'glyphicon glyphicon-remove-circle'; 
        } 
 
        c.html('<i class="' + css + '"></i>'); 
      } 

Update bundleconfig.json so that it includes the new CheckTurnIsFinished.js file:

{
"outputFileName": "wwwroot/js/site.js",
"inputFiles": [
"wwwroot/app/js/scripts1.js",
"wwwroot/app/js/scripts2.js",
"wwwroot/app/js/GameSession.js",
"wwwroot/app/js/CheckTurnIsFinished.js"
],
"sourceMap": true,
"includeInProject": true
},
  1. Update the SetGameSession method in the GameSession.js JavaScript file. Now, set TurnNumber to 0 by default:
        function SetGameSession(gdSessionId, strEmail) { 
          window.GameSessionId = gdSessionId; 
          window.EmailPlayer = strEmail; 
          window.TurnNumber = 0; 
        } 
  1. Update the SendPosition function in the GameSession.js JavaScript file and remove the temporary testing alert box we added previously. The game will be fully functional by the end of this section:
// Remove this alert        
'success': function (data) {
alert(data);
}
  1. Now, we need to add two new methods to GameSessionController. The first one is called CheckGameSessionIsFinished and uses the game session service to get the session and decide whether the game was a draw or was won by user 1 or 2. As a result, the system will know whether the game session has finished. To do this, use the following code:
[Produces("application/json")]
[HttpGet("/restapi/v1/CheckGameSessionIsFinished/{sessionId}")]
public async Task<IActionResult> CheckGameSessionIsFinished(Guid sessionId)
{ if (sessionId != Guid.Empty)
{
var session = await
_gameSessionService.GetGameSession(sessionId);
if (session != null)
{
if (session.Turns.Count() == 9) return Ok("The
game was a draw.");
var userTurns = session.Turns.Where(x => x.User ==
session.User1).ToList();
var user1Won = CheckIfUserHasWon(session.User1?.Email,
userTurns);
if (user1Won) return Ok($"{session.User1.Email} has
won the game.");
else
{
userTurns = session.Turns.Where(x => x.User ==
session.User2).ToList();
var user2Won = CheckIfUserHasWon(session.User2?.
Email, userTurns);

if (user2Won)return Ok($"{session.User2.Email}
has won the game.");
else return Ok("");
}
}
else
return NotFound($"Cannot find session {sessionId}.");
}
else
return BadRequest("SessionId is null.");
}

Now, we need to implement the second method, that is, CheckIfUserHasWon, which determines whether a user has won the game and sends this information to GameSessionController

private bool CheckIfUserHasWon(string email,
List<TurnModel> userTurns) { if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 0) &&
userTurns.Any(x => x.X == 2 && x.Y == 0)) return true; else if (userTurns.Any(x => x.X == 0 && x.Y == 1) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 1)) return true; else if (userTurns.Any(x => x.X == 0 && x.Y == 2) &&
userTurns.Any(x => x.X == 1 && x.Y == 2) &&
userTurns.Any(x => x.X == 2 && x.Y == 2)) return true; else if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 0 && x.Y == 1) &&
userTurns.Any(x => x.X == 0 && x.Y == 2)) return true; else if (userTurns.Any(x => x.X == 1 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 1 && x.Y == 2)) return true; else if (userTurns.Any(x => x.X == 2 && x.Y == 0) &&
userTurns.Any(x => x.X == 2 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 2)) return true; else if (userTurns.Any(x => x.X == 0 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 2 && x.Y == 2)) return true; else if (userTurns.Any(x => x.X == 2 && x.Y == 0) &&
userTurns.Any(x => x.X == 1 && x.Y == 1) &&
userTurns.Any(x => x.X == 0 && x.Y == 2)) return true; else return false; }
  1. Add a new JavaScript file called CheckGameSessionIsFinished.js to the wwwrootappjs folder and update the bundleconfig.json file accordingly:
        function CheckGameSessionIsFinished() { 
          var port = document.location.port ? (":" +  
document.location.port) : ""; var url = document.location.protocol + "//" +
document.location.hostname + port +
"/restapi/v1/CheckGameSessionIsFinished/" +
window.GameSessionId; $.get(url, function (data) { debugger; if (data.indexOf("won") > 0 || data == "The game
was a draw.") { alert(data); window.location.href = document.location.protocol +
"//" + document.location.hostname + port; } }); }
  1. Start the game, register a new account, open the confirmation email, confirm it, send a game invitation email, confirm the game invitation, and start playing. Everything should be working now, and you should be able to play the game until a user has won or until the game ends in a draw:

In this section, we've looked at the RPC-style, which is very close to standard MVC Controller actions. In the following sections, you learn about a completely different approach, which is based on resources and resource management.

Congratulations; you have finished the implementation of RPC-style and created a beautiful, modern, browser-based game in which two users can play against each other.

Prepare yourself – in the following sections, you're going to look at more advanced techniques and discover how to provide web APIs for interoperability using two of the most famous API communication styles: REST and HATEOAS.

To play the game, you can either use two separate private browser windows or use two distinct browsers, such as Chrome, Edge, or Firefox. To test your web APIs, it is advised that you install and use Postman (https://www.getpostman.com/), but you can also use any other HTTP REST-compatible client, such as Fiddler (https://www.telerik.com/fiddler), SoapUI (https://www.soapui.org/downloads/soapui.html), or even Firefox via its advanced features.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.57.219