Retry and Circuit Breaker Policy example .Net 6 and Polly

In this example, we’ll implement the Wait and Retry and Circuit Breaker policy using .Net 6 Web API and Polly. For more details on what Circuit Breaker is, refer to the MSDN documentation.

Create a WebAPI with ValuesController in .Net 6 which will always return an Exception in the Get call.

using Microsoft.AspNetCore.Mvc;

namespace ExternalAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ValuesController : ControllerBase
    {

        private readonly ILogger<ValuesController> _logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            throw new Exception("Error");
        }

    }
}

Let’s call this ExternalAPI and running on https://localhost:7250/.

Create another .Net 6 WebAPI called CircuitBreaker.Demo and install Polly and Newtonsoft.json Nuget packages.

Configure Polly and HttpClient in Program.cs file as shown below:

using Polly;
using Polly.Extensions.Http;

var builder = WebApplication.CreateBuilder(args);
var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt));

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient("errorApi", c => { c.BaseAddress = new Uri("https://localhost:7250"); });
builder.Services.AddSingleton<IAsyncPolicy<HttpResponseMessage>>(httpRetryPolicy);


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Now add the PollyController to call the ExternalAPI using the HttpClient:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Polly;

namespace CircuitBreaker.Demo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class PollyController : ControllerBase
    {

        private readonly ILogger<PollyController> _logger;
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IAsyncPolicy<HttpResponseMessage> _policy;

        public PollyController(ILogger<PollyController> logger, IHttpClientFactory httpClientFactory, IAsyncPolicy<HttpResponseMessage> policy)
        {
            _logger = logger;
            _httpClientFactory = httpClientFactory;
            _policy = policy;
        }

        [HttpGet(Name = "GetApiResult")]
        public async Task<ActionResult<IEnumerable<string>>> Get()
        {
            var client = _httpClientFactory.CreateClient("errorApi");
            var response = await _policy.ExecuteAsync(() => client.GetAsync("values"));
            response.EnsureSuccessStatusCode();
            return JsonConvert.DeserializeObject<string[]>(await response.Content.ReadAsStringAsync());
        }

    }
}

When you run both the APIs, the call to External APIs is automatically made using Wait and Retry policy after every retryAttempt number of seconds 3 times. After that, the calls are stopped as the ExternalAPI keeps giving 500 error.

CircuitBreaker example:

Now change the Http Retry Policy line in the Program.cs file to:

var httpRetryPolicy = Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
    .CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));

The above line will open the circuit and will start giving Circuit Open Exception after 2 attempts and keep the circuit Open for 30 seconds, which means you cannot make further calls to the API for that duration. The Exception will now be shown as below:

Microservices with .Net Core API and Azure Service Bus

Basic Microservice Architecture with Azure Service Bus and API Gateway

The above Architecture has the following components:

Front-end: ReactJS Application with a simple form hosted on App Service Plan which will hit the API Gateway with GET/POST requests.

Name Microservice: First Microservice hosted on App Service Plan that will take User Details from front-end and save PersonId and Name in it’s own Azure SQL database.

Azure Service Bus: The Name Microservice will forward the PersonId created and Address details from front-end to the Azure Service Bus Topic.

Address Microservice: Second Microservice hosted on App Service Plan will subscribe to the Topic that takes PersonId and Address data from the Name service and store the details in it’s own Azure SQL database.

This is not an ideal scenario for creating Microservice Architecture but it shows the idea how the individual components interact with each other.

*In addition to this, another Azure Function will subscribe to the Queue for new User creation and sends email to someone with the details. This is however, not shown in the above image.

The Front-end Application is a form created in ReactJS that takes in following fields from the User:

FirstName, MiddleName, LastName, Email, Phone Number and Address

The following front-end and back-end repositories have all the code for connecting to the required components.

Further, a separate Authentication Microservice can be created which will return a JWT token based on the credentials provided. The token can then be validated by the API Gateway configured with the API Manager Service and then allow the requests to go through to the other Microservices.

The front-end code does not have Login page in this sample, however it directly passes the credentials to the Auth Service to generate the token. The token is then passed through to the subsequent calls to the API Management Gateway.

Select Validate JWT policy for Inbound Processing at the Operation Level for APIs under API Management.

Change the issuer signing key while adding the Inbound Policy under API Management as per the key used while generating the token from your Authentication Microservice.

<inbound>
        <base />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized">
            <issuer-signing-keys>
                <key>xxxxxxxxxxx</key>
            </issuer-signing-keys>
        </validate-jwt>
    </inbound>

Also, make sure the CORS policy is updated under the respective APIs Inbound Processing at the required operations or All Operations level:

<cors allow-credentials="false">
            <allowed-origins>
                <origin>https://mywebsite.azurewebsites.net</origin>
                <origin>http://localhost:3000</origin>
                <origin>https://mypoc.servicebus.windows.net</origin>
            </allowed-origins>
            <allowed-methods>
                <method>GET</method>
                <method>POST</method>
                <method>OPTIONS</method>
            </allowed-methods>
            <allowed-headers>
                <header>Authorization</header>
                <header>Content-Type</header>
            </allowed-headers>
        </cors>

Update the connection strings as per your Database and Service Bus Services from the Code Repository provided above.

Another example of Microservices Architecture for a real world scenario is as below:

Microservices Architecture for Online Shopping Application

Check email count on mailbox with C#

Below is an example of .Net core 3.1 Console App that is reading a mailbox on Exchange and checking the count of mails. If the mail count cross a certain threshold, it’ll check for an e-mail older than 2 hours to send an alert.

Such an example can be used to schedule alerts or log information if you’re tracking mails in a mailbox.

Install Nuget package Microsoft.Exchange.WebServices to use ExchangeService class.

using Microsoft.Exchange.WebServices.Data;

public static void Main(string[] args)
{
	string TextlogMessage;
	string Subject = "";
	string sUsername = ConfigurationManager.AppSettings.Get("UserName");
	var section = ConfigurationManager.GetSection("secureAppSettings") as NameValueCollection;
	string vCount = ConfigurationManager.AppSettings.Get("Count");
	int Count = Convert.ToInt32(vCount);
	string sPassword = ConfigurationManager.AppSettings.Get("Password");
	
	string sDomain = ConfigurationManager.AppSettings.Get("Domain");
	TimeZoneInfo INDIAN_ZONE = TimeZoneInfo.FindSystemTimeZoneById("India Standard Time");
	DateTime indianTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, INDIAN_ZONE);
	DateTime comparedate = indianTime.AddHours(-2);
	DateTime MailDateTime = comparedate;
	ExchangeService exchange = new ExchangeService(ExchangeVersion.Exchange2013_SP1);
	exchange.Credentials = new WebCredentials(sUsername, sPassword, sDomain);
	
	exchange.Url = new Uri(ConfigurationManager.AppSettings.Get("ExchangeUrl"));
	Folder inbox = Folder.Bind(exchange, WellKnownFolderName.Inbox);
	var oSendemail = 0;
	TextlogMessage = "Start With count:" + inbox.TotalCount;




	if (inbox.TotalCount > Count)
	{

		Console.WriteLine("Count " + inbox.TotalCount);

		ItemView view = new ItemView(1);

		view.OrderBy.Add(ItemSchema.DateTimeReceived, SortDirection.Ascending);
		
		var findResults = exchange.FindItems(WellKnownFolderName.Inbox, view);


		foreach (Item item in findResults.Items)
		{

			Console.WriteLine("Test: " + item.DateTimeReceived + " %" + comparedate);
			DateTime newdate = TimeZoneInfo.ConvertTimeToUtc(item.DateTimeReceived);

			int Dresult = DateTime.Compare(newdate, comparedate);
			if (Dresult < 0)
			{
				Console.WriteLine("Alert " + "Sub" + item.Subject + newdate + " %" + comparedate);
				oSendemail = 1;
				MailDateTime = newdate;
				Subject = item.Subject;


			}

		}
		if (oSendemail > 0)
		{
			Program p = new Program();
			//To Do Send Email Logic..
			p.SendEmail(exchange, oSendemail, inbox.TotalCount, TextlogMessage);
			TextlogMessage = TextlogMessage + " EmailTime: " + MailDateTime + "CompareTime " + comparedate + "  Alert Sent Subject : " + Subject;

		}

	}

	//To Do logging..
	log(TextlogMessage);

}

Call .Net core API from Console with App Bearer token

In the following example, we’re using a .Net Core 3.1 Console App that will call API with POST request that requires Authentication with a bearer token in Authrorization Header. The token is generated by passing credentials to another API endpoint.

For more details on how to use appSettings.json file in Console App, check this post.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;

namespace ConsoleApp1
{

    class Credentials
    {
        public string username { get; set; }
        public string password { get; set; }
    }

    class Token
    {
        public string token { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string ResponseString = "";
            HttpWebResponse response = null;
            HttpWebResponse response2 = null;
            IConfiguration Config = new ConfigurationBuilder()
                .AddJsonFile("appSettings.json")
                .Build();

            try
            {
                var baseURL = Config.GetSection("baseURL").Value;
                var request = (HttpWebRequest)WebRequest.Create(baseURL + "/token");
                request.Accept = "application/json"; //"application/xml";
                request.Method = "POST";

		//Get credentials from config.
                var dusername = EncryptionService.Decrypt(Config.GetSection("credentials")["username"]);
                var dpassword = EncryptionService.Decrypt(Config.GetSection("credentials")["password"]);

                Credentials cred = new Credentials()
                {
                    username = dusername,
                    password = dpassword
                };

                var myContent = JsonConvert.SerializeObject(cred);

                var data = Encoding.ASCII.GetBytes(myContent);

                request.ContentType = "application/json";
                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                }

                using (response = (HttpWebResponse)request.GetResponse())
                {
                    ResponseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
                }

		//Get the token from the /token end-point and call another end-point.
                Token token = JsonConvert.DeserializeObject<Token>(ResponseString);

                var request2 = (HttpWebRequest)WebRequest.Create(baseURL + "/ProcessData");
                request2.Accept = "application/json"; //"application/xml";
                request2.Method = "POST";
				
		//Pass token in Authorization Header.
                request2.Headers["Authorization"] = "Bearer " + token.token;

                using (response2 = (HttpWebResponse)request2.GetResponse())
                {
                    ResponseString = new StreamReader(response2.GetResponseStream()).ReadToEnd();
                }

                Console.WriteLine(ResponseString);
                Environment.Exit(0);
            }
            catch (WebException ex)
            {
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    response = (HttpWebResponse)ex.Response;
                    ResponseString = "Some error occured: " + response.StatusCode.ToString();
                }
                else
                {
                    ResponseString = "Some error occured: " + ex.Status.ToString();
                }
            }

        }
    }

}

Use config file in .Net core Console App

For this example, I’m using a Console App created in .Net Core 3.1 using Visual Studio 2019.

Add a json file to your Project and name it appSettings.json, it could like like below:

{
  "credentials": {
    "username": "xxxx",
    "password": "xxxx"
  },
  "URL": "https://workdayapipoc.egonzehnder.com"
}

Install the following Nuget packages in your Project:

<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />

The versions could vary depending on the time you’re adding these packages.

In your Program.cs file, add the namespace:

using Microsoft.Extensions.Configuration;

In the Main method, add the following code:

class Program
{
    static void Main(string[] args)
    {
		//....
		
		IConfiguration Config = new ConfigurationBuilder()
                .AddJsonFile("appSettings.json")
                .Build();
				
		var URL = Config.GetSection("URL").Value;
		
		//Assuming you're using Encrypted values in configuration.
		var dusername = EncryptionClass.Decrypt(Config.GetSection("credentials")["username"]);
        var dpassword = EncryptionClass.Decrypt(Config.GetSection("credentials")["password"]);
		
		//.....
		
    }
}

Show multiline text svg circle

For creating a circle using svg, you can follow this post. Then add the below code within the svg view box.

Normally you can use text tag to show some text in a svg component. But in order to show multiline text, we need to use HTML inside foreignObject tag as shown below:

< foreignObject id="content" x="20" y="25" width="100" height="100" >
                    < div >
                        < p id="txt1" style="font-size: 12px;">Text 1< /p >
                        < p id="txt2" style="font-size: 12px;">Text 2< /p >
                    < /div >
                < /foreignObject >

The x, y values should be set according to the svg view box. You can also use normal Javascript to modify the text using innerText property as below:

document.getElementById("txt1").innerText = "New Text 1";

Circular Progress bar svg javascript

In this post, I’ll give the code to create a Circular progress bar which I tried with a Start button and it resets every time you click on the start button and ticks every second for 60 seconds. The start button is shown using media control html code. The start button will re-appear once timer is reset.

If you’re using Asp.net Core MVC template, I’ve put the below code in index.cshtml file.

The HTML is as below:

< div >
            < svg class="progress-ring"
                 width="120"
                 height="120" >
                < circle class="progress-ring__circle"
                        stroke="orange"
                        stroke-width="4"
                        fill="transparent"
                        r="58"
                        cx="60"
                        cy="60" / >
                < text id="play" x="40" y="70" onclick="startTimer()" >▶< /text >
            < /svg >
< /div >

Css is as follows in the site.css file:

.progress-ring {
}

.progress-ring__circle {
    transition: 0.35s stroke-dashoffset;
    transform-origin: 50% 50%;
}

#play {
    cursor: pointer;
    font-size: xx-large;
}

The below Javascript code is going to modify the svg stroke-dashoffset attribute as below in the site.js file:

var i = 0;
var interval;
var circle = document.querySelector('circle');
var radius = circle.r.baseVal.value;
var circumference = radius * 2 * Math.PI;
console.log('radius', radius);
console.log('circumference', circumference);
circle.style.strokeDasharray = `${circumference} ${circumference}`;
circle.style.strokeDashoffset = `${circumference}`;

function setProgress(percent) {
    const offset = circumference - percent / 100 * circumference;
    circle.style.strokeDashoffset = offset;
}

function startTimer() {
    console.log('circumference', circumference);
    circle.style.strokeDashoffset = `${circumference}`;
    document.getElementById("play").textContent = "ok";
    interval = setInterval(increment, 1000);
}

function increment() {
    i = i % 360 + 1;
    var perc = (i / 60) * 100;
    console.log(i);
    if (i === 60) {
        clearInterval(interval);
        document.getElementById("play").textContent = "\u25B6";
        i = 0;
    }
    setProgress(perc);
}

The stroke-dashoffset value is reduced every time to increase the progress with stroke-dasharray. You can play around with the radius to increase the circle size.

Upload word document to .Net core WebAPI

We’ll be working with a .docm template in Word 2016 which contains a textbox. This control is given a name using the Content Control Properties.

This is available under Developer Menu in MS Word, then click on Properties.

Save this template as .docm

Create .Net Core WebAPI under .net core version 3.1 for this example. The sample code is as below. The library System.IO is used to access the value in the variable.

using System;
using System.IO;
using System.IO.Packaging;
using System.Xml;
using System.Xml.XPath;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

namespace SampleApplicationUpload.Controllers
{
    public class UploadController : ControllerBase
    {
        [HttpPost]
        [Route("upload")]
        [ProducesResponseType(StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public IActionResult Upload()
        {
            string filename = string.Empty;
            string extension = string.Empty;
            string result = string.Empty;
            bool uploaded = false;
            string ModifiedBy = string.Empty;
            var request = HttpContext.Request;

            if (request.Form.Files.Count > 0)
            {
                filename = request.Form.Files[0].FileName;
                extension = filename.Substring(filename.LastIndexOf(".")).ToLower();
                if (!string.IsNullOrEmpty(extension) && extension.Equals(".docm"))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        request.Form.Files[0].CopyTo(memoryStream);
                        if (memoryStream.Length < 2097152)
                        {
                            result = UploadWorStream(memoryStream, extension, out ModifiedBy, out uploaded);
                        }
                        else
                        {
                            return BadRequest("File size too large");
                        }
                    }
                }
                else
                {
                    return BadRequest("File format not recognised. Upload .docm file only");
                }
            }           
            return Ok(result);
        }
        private string UploadWorStream(MemoryStream ms, string ext, out string ModifiedBy, out bool uploaded)
        {
            string contenttype = string.Empty;
            bool erroroccurs = false;
            ModifiedBy = string.Empty;
            var xmlDoc = new XmlDocument();
            uploaded = false;
            switch (ext)
            {
                case ".docm":
                    contenttype = "application/vnd.ms-word";
                    break;
                default:
                    erroroccurs = true;
                    break;
            }

            if (!erroroccurs)
            {
                if (contenttype != string.Empty)
                {
                    Stream fs = ms;
                    var br = new BinaryReader(fs);
                    string xmlDocRelType = "http://schemas.openxmlformats.org/officeDocument/" +
                                           "2006/relationships/customXml";
                    string officeDocRelType = "http://schemas.openxmlformats.org/" +
                                              "officeDocument/2006/relationships/officeDocument";
                    PackagePart xmlPart = default(PackagePart);
                    PackagePart documentPart = default(PackagePart);
                    Uri documentUri = default(Uri);
                    Uri xmlUri = default(Uri);
                    using (Package officePackage = Package.Open(fs, FileMode.Open, FileAccess.Read))
                    {
                        foreach (var relationship1 in officePackage.GetRelationshipsByType(officeDocRelType))
                        {
                            documentUri = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative),
                                                                       relationship1.TargetUri);
                            documentPart = officePackage.GetPart(documentUri);
                            break;
                        }

                        if (documentPart != null)
                            foreach (var relationship2 in documentPart.GetRelationshipsByType(xmlDocRelType))
                            {
                                xmlUri = PackUriHelper.ResolvePartUri(new Uri("/", UriKind.Relative),
                                                                      relationship2.TargetUri);
                            }
                        xmlPart = officePackage.GetPart(xmlUri);
                        Stream inputStream = xmlPart.GetStream(FileMode.Open, FileAccess.Read);
                        var ts = new StreamReader(inputStream);
                        xmlDoc.Load(ts);
                        ts.Close();
                        ts.Dispose();
                        officePackage.Close();
                    }

                    XPathNavigator xpnGet2 = xmlDoc.CreateNavigator();
                   // string txtSample = xpnGet2.Evaluate("string(//ROOT[1]/SampleDoc[1]/@txtSample)").ToString();
                    string txtSample = Convert.ToString(xpnGet2.Evaluate("string(//ROOT/SampleDoc/@txtSample)"));
                    if (txtSample.Length > 0)
                    {
                        return txtSample;
                    }
                    else {
                        return "Unable to read document";
                    }
                }
                else
                {
                    uploaded = false;
                    return "Error";
                }
            }
            else
            {
                uploaded = false;
                return "Error";
            }
        }
    }
}

Finally try to access the WebAPI using a Postman request and check the value while debugging the end-point.

curl --location --request POST 'https://localhost:<port>/upload' \
--form 'attachment
=@/C:/Users/username/Desktop/TestDocument.docm'

Upload file in ReactJS to WebAPI

Create a new React App using create-react-app as explained in this Post. The sample ReactJS code is available here.

In the index.js file change the code as below:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

For the App.js file, change the code as below:

import React from 'react';       
import './App.css';
import Fileupload from './FileUpload'
function App() {    
  return (    
    <div className="App">    
      <Fileupload></Fileupload>
    </div>    
  );    
}
export default App;

Now comes the FileUpload component as FileUpload.js:

import React from 'react';
import axios from 'axios';

class Fileupload extends React.Component {

    constructor(props) {    
            super(props);    
            this.state = {    
                    file: '',    
        };    
    }

    async submit(e) {    
        e.preventDefault();    
        const url = `http://url`;    
        const formData = new FormData();    
        formData.append('body', this.state.file);    
        const config = {    
            headers: {    
                'content-type': 'multipart/form-data',
                'token': 'xxx'                
            },
        };
        const HTTP = axios.create({
            withCredentials: true
        });

        //return post(url, formData, config);
        return HTTP.post(url, formData, config);
    }
    setFile(e) {    
        this.setState({ file: e.target.files[0] });    
    }

    render() {    
        return (    
                <div className="container-fluid">    
                        <form onSubmit={e => this.submit(e)}>    
                                <div className="col-sm-12 btn btn-primary">    
                                        File Upload    
                        </div>    
                                <h1>File Upload</h1>    
                                <input type="file" onChange={e => this.setFile(e)} />    
                                <button className="btn btn-primary" type="submit">Upload</button>    
                        </form>    
                </div>    
        )    
    }
}
export default Fileupload

Here I’m using axios to call the API via Post request. Passing the credentials in the call with create.
The token part is optional in the Headers, you can add/remove headers based on your requirement.

From the WebAPI end, I’m using .Net core. You can receive the file in the Action method as below:

[HttpPost]
[Route("upload")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public IActionResult UploadForm([FromQuery] string username)
{
	var request = HttpContext.Request;
            
	if (request.Form.Files.Count > 0)
	{
		filename = request.Form.Files[0].FileName;
		extension = filename.Substring(filename.LastIndexOf(".")).ToLower();
		if (!string.IsNullOrEmpty(extension) && extension.Equals(".docx"))
		{
			using (var memoryStream = new MemoryStream())
			{
				request.Form.Files[0].CopyTo(memoryStream);
				
				// Upload the file if less than 2 MB
				if (memoryStream.Length < 2097152)
				{
					string check = Service.CheckifValid(username);
					if (check.Equals("ok"))
					{
						//Process the stream and pass the data to the back-end in the Service Layer.
						result = Service.UploadStream(memoryStream, filename, extension, username);
					}
					else
					{
						return Ok(check);
					}
				}
				else
				{
					return BadRequest("File size too large");
				}
			}
		}
		else
		{
			return BadRequest("File format not recognised.");
		}
	}
	//Other ToDos...
}