"тнιѕ вℓσg ¢συℓ∂ ѕανє уσυя мσηєу ιƒ тιмє = мσηєу" - ∂.мαηנαℓу

Wednesday 30 December 2020

Tips for Implementing Service Protection API Limits with Dynamics 365 CRM Web API Scenario

Dynamics CRM API limits is a big topic. If you are not familiar what are the API limits for Dynamics 365, it is described here in a nutshell ( https://crmdm.blogspot.com/2020/11/dynamics-365-api-limits.html?m=0 )

 This post aims to provide some tips for implementing the service protection API limits along with Dynamicis CRM Web API scenario.

 In simple words, if your API requests exceeds the API limits mentioned below, your system would be throttled. API request returns a famous error called 'Too Many Requests' / 429 error.
 

"API limits enforced per web server within the 5 minute sliding window.

Measure Description Limit per web server
Number of requests The cumulative number of requests made by the user. 6000
Execution time The combined execution time of all requests made by the user. 20 minutes (1200 seconds)
Number of concurrent requestsThe number of concurrent requests made by the user52"

( Ref:https://docs.microsoft.com/en-us/powerapps/developer/data-platform/api-limits#retry-operations)

 Solution is mentioned by Microsoft in the above link. Basically the error response would have something like a header called 'Retry-After'. It would also have a retry after time in seconds.

I have some code snippets that I tried based on Microsoft documentation+ improved+unit tests.

 As per Microsoft documentation, all requests go through HttpClient.SendAsync method. Here is a small diagram to understand this concept. One main advantage of this implementation is that Retry implementation can be done in one place.HttpClient.SendAsync() is a generic method. In other words, it's wrapping common operations with Http method.


For instance, a read operation is GetAsync. Eventhough HttpClient has a GetAsync method, it should go through HttpClient.SendAsync() as shown.  HttpClient.SendAsync() captures 429 error and 'Retry-After' values accurately. So it is possible to set up the retry mechanism on the HttpClient.SendAsync(). And Retry is needed only in one place because rest of the methods call this one.

So make sure that only SendAsync() is called for HttpClient request.Code sample is below.

 For understanding tokens part , please refer this post - (https://crmdm.blogspot.com/2020/12/dynamics-365-crm-web-api-with-azure.html?m=0 ). You would get a better idea. For unit tests, it's possible to mock the token service.

Maximum Retry is set to 3 in this sample. But feel free to change it accordingly.

using Demo.Core.Helper;
using Demo.Core.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Demo.Core.Services
{
    public class HttpClientService : IHttpClientService
    {
        public HttpClient HttpClient { get; set; }
        private readonly ITokenService _tokenService;
        private readonly ILogger _logger;

        public HttpClientService(ITokenService tokenService, ILogger<HttpClientService> logger)
        {
            _tokenService = tokenService ?? throw new ArgumentNullException(nameof(tokenService));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));

            HttpClient = new HttpClient();
            HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var token = _tokenService.GenerateToken().Result;
            HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
                token);
        }

        public async Task<HttpResponseMessage> GetAsync(string requestUri)
        {
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri);
            HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead;
            return await SendAsync(request, completionOption);
        }

        private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseHeadersRead, int retryCount = 0)
        {
            var maxRetry = 3;
            HttpResponseMessage response = null;
            try
            {
                response = await HttpClient.SendAsync(request.Clone(), httpCompletionOption);
            }
            catch (Exception e)
            {
                _logger.LogError(e.Message);
            }

            if (!response.IsSuccessStatusCode)
            {
                if (response.StatusCode != HttpStatusCode.TooManyRequests)
                {
                    throw new HttpRequestException();
                }
                else
                {
                    if (++retryCount >= maxRetry)
                    {
                        throw new HttpRequestException();
                    }
                    var seconds = (response.Headers.Contains("Retry-After"))?
                        int.Parse(response.Headers.GetValues("Retry-After").FirstOrDefault())
                            :(int)Math.Pow(2, retryCount);
                    Thread.Sleep(TimeSpan.FromSeconds(seconds));

                    return await SendAsync(request, httpCompletionOption, retryCount);
                }
            }
            else
            {
                return response;
            }
        }
    }
}

As you could see in the code, it sends a request copy rather than actual request. This is because request wouldn't be available if error occurs.A function called Clone takes the copy of the request. This is provided by Microsoft. Didn't make any changes to it. Code snippet is below ( Ref:Microsoft)

using System;
using System.Collections.Generic;
using System.Net.Http;

namespace Demo.Core.Helper
{
    /// <summary>
    /// Contains extension methods to clone HttpRequestMessage and HttpContent types.
    /// </summary>
    public static class Extensions
    {

        /// <summary>
        /// Clones a HttpRequestMessage instance
        /// </summary>
        /// <param name="request">The HttpRequestMessage to clone.</param>
        /// <returns>A copy of the HttpRequestMessage</returns>
        public static HttpRequestMessage Clone(this HttpRequestMessage request)
        {
            var clone = new HttpRequestMessage(request.Method, request.RequestUri)
            {
                Content = request.Content.Clone(),
                Version = request.Version
            };
            foreach (KeyValuePair<string, object> prop in request.Properties)
            {
                clone.Properties.Add(prop);
            }
            foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
            {
                clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }

            return clone;
        }
        /// <summary>
        /// Clones a HttpContent instance
        /// </summary>
        /// <param name="content">The HttpContent to clone</param>
        /// <returns>A copy of the HttpContent</returns>
        public static HttpContent Clone(this HttpContent content)
        {

            if (content == null) return null;

            HttpContent clone;

            switch (content)
            {
                case StringContent sc:
                    clone = new StringContent(sc.ReadAsStringAsync().Result);
                    break;
                default:
                    throw new Exception($"{content.GetType()} Content type not implemented for HttpContent.Clone extension method.");
            }

            clone.Headers.Clear();
            foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
            {
                clone.Headers.Add(header.Key, header.Value);
            }

            return clone;

        }
    }
}

Wrote a couple of unit tests to test this scenario. Thanks to this post (ref: https://www.c-sharpcorner.com/article/mocking-httpclient-using-xunit-in-net-core/

using AutoFixture;
using Demo.Core.Interfaces;
using Demo.Core.Services;
using Microsoft.Extensions.Logging;
using Moq;
using Moq.Protected;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Demo.Core.Unit.Tests
{
    public class HttpClientServiceTests
    {
        private readonly Mock<ILogger<HttpClientService>> _mockLogger;
        private readonly Mock<ITokenService> _mockTokenService;
        private HttpClientService _sut;
        public HttpClientServiceTests()
        {
            _mockLogger = new Mock<ILogger<HttpClientService>>();
            _mockTokenService = new Mock<ITokenService>();
            _sut = new HttpClientService(_mockTokenService.Object, _mockLogger.Object);
        }

        [Fact]
        public async Task GetAsyncReturnsSuccessfully()
        {
            var expected = "I am Tom";
            var Result = new StringContent(expected);
            var httpMessageHandler = new Mock<HttpMessageHandler>();
            var fixture = new Fixture();

            httpMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>(
                    "SendAsync",
                    ItExpr.IsAny<HttpRequestMessage>(),
                    ItExpr.IsAny<CancellationToken>()
                )
                .ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>
                {
                    HttpResponseMessage response = new HttpResponseMessage();
                    response.StatusCode = HttpStatusCode.OK;
                    response.Content = Result;
                    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                    return response;
                });

            var httpClient = new HttpClient(httpMessageHandler.Object);
            httpClient.BaseAddress = fixture.Create<Uri>();
            _sut.HttpClient = httpClient;

            var response = await _sut.GetAsync(string.Empty);
            var actual = await response.Content.ReadAsStringAsync();

            Assert.Equal(actual, expected);
        }

        [Fact]
        public async Task GetAsyncReturnsTooManyRequestsThrowsException()
        {

            var expected = "I am Tom";
            var Result = new StringContent(expected);
            var httpMessageHandler = new Mock<HttpMessageHandler>();
            var fixture = new Fixture();
            var waitTimeInSeconds = 5;
            httpMessageHandler.Protected()
                .Setup<Task<HttpResponseMessage>>(
                    "SendAsync",
                    ItExpr.IsAny<HttpRequestMessage>(),
                    ItExpr.IsAny<CancellationToken>()
                )
                .ReturnsAsync((HttpRequestMessage request, CancellationToken token) =>
                {
                    HttpResponseMessage response = new HttpResponseMessage();
                    response.StatusCode = HttpStatusCode.TooManyRequests;
                    response.Content = Result;
                    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                    response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(waitTimeInSeconds));
                    return response;
                });

            var httpClient = new HttpClient(httpMessageHandler.Object);
            httpClient.BaseAddress = fixture.Create<Uri>();
            _sut.HttpClient = httpClient;

            await Assert.ThrowsAsync<HttpRequestException>(() => _sut.GetAsync(string.Empty));
        }
    }
}

 Green is good!

Tuesday 29 December 2020

Dynamics 365 CRM Web API with Azure Function - Nail it!

As you all know there are mainly two connectivity options are available for our apps ( Azure function, Console app, mobile app, web apps etc) to communicate with Dynamics 365 CRM.

  • Organization service - SOAP service 
  • Dynamics 365 CRM Web API - HTTP REST API

Organization service has some limitations. For instance, you could only use a .Net Frame work based app with organisation service. Also in my view, it is getting less popular day by day.

Dynamics 365 CRM Web API has a lot of cool features. One of the main advantages is that you could use .Net core based apps ( for instance, azure functions, web apps etc)to connect to Dynamics CRM. Third party systems can utilise Crm Web Apis to communicate with Dynamics 365 CRM. Using a timer triggered .Net core azure function we could easily demonstrate this. Main steps are below. Detailed steps and sample code are also provided in this post.

  • How to generate a token from Azure AD for Dynamics 365 CRM Web API
  • Use the token to call a simple Dynamics CRM Web Api  - Who am I- It returns some basic info like your organizationid,userid etc

First things first , let's understand the architecture first. I have created an architecture diagram to understand this functionality. Please note that the  Azure AD and Dynamics 365 CRM are under the same tenant. 

As you could see in the diagram, Azure function requests the token from Azure AD first. Using this token it can do API calls( CRUD - Create, Read, Update and Delete operations) to Dynamics 365 CRM.  

Tokens life time: By default Azure AD tokens expire in one hour (ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-configurable-token-lifetimes#access-tokens)

Azure AD Part:

From Azure we need the below values in order to retrieve a token.

  • Tenant Id
  • Application Id
  • Client Secret

In order to get these values we need to do some configurations. These are shown in the diagram. But now let's do it step by step.

Navigate to  https://portal.azure.com/  and look for Active Directory.

 

 We get the Tenant Id straightaway as shown in the picture. That was easy! one down!
 

 

Navigate to App Registrations on the left navigation.



Click on New Registration.


Register a new application with a preferred name.

 


And we get the Application Id from here.



Next step is to set up API permission for our Dynamics 365 CRM instance. So let's navigate to API Permissions.

Click on Add a permission option.


Let's choose Dynamics CRM from the list. You could also see that other app registrations are possible here. Our focus is Dynamics CRM.


Tick on the user_impersonation option and click on Add permission. It means that this application is registered here and it can access Dynamics CRM as a user (ref:https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)


Next step is to define a new client secret for our app registration.Click on the Certificates & Secrets option. And then click on the New Client Secret button.

It is possible to set expiry for the secret. In our case, it doen't matter. Just a description and choose the expiry as per our preference.

Now there is an important bit here. We need only Client secret value not the ID displayed there. I didn't have to use that anywhere.


Azure set up is done here. 

Dynamics CRM Part:

Next part is Dynamics 365 CRM. So we need to create a new 'application user' in Dynamics. This user is created based on the application id from the Azure App registration that we did earlier.

Navigate to Dynamics CRM->Settings->Security->Users and choose application users.


Click on the New button


Here is a tricky part. We need to make sure that we switch forms to Application user form.

Application Id - The one from Azure app registrion. Please refere previous step.

User Name, Primary Email - I usally put something like this

preferredusername@mycrmtrialorganisationname.onmicrosoft.com

For instance, testuser1@mytrialcrm.onmicrosoft.com ( Refer your trial crm login)

Full Name - As you prefer.

After providing all the above details, save the record.

If the user creation is successul, you should see Application ID URI and Azure AD Object ID auto populated.


Next step is to give access to the application user. As you could imagine, a user would need security roles in order to do CRUD ( Create, Read, Update, Delete)operations. 


So in this case let's assign system administrator role to this user.


We need one more info from Dynamics CRM. Basically we need Crm web api base url also known as service root Url. Navigate to Developer resources as shown below.


Copy the service root url. This is needed in our web api request for whoami.



I hope that you can relate all the steps with the architecture diagram now. Simple demo built on the above steps is given below. It is a simple Web API call. But you could expand it. For instance, create contact , lead etc.

Sample azure function here is based on .Net Core 3.1 and has a timer trigger of 1 minute. This means it gets triggered every minute. You could run it locally easily.

Azure functions have a settings file. It is handy to define all the config values there and retrieve when needed.

local.settings json file is here.

Oranganization url is your crm url, Aad Instance is the valuei in the below screen shot. It is a generic value.



Here is the full source code

using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace AzureTimerTriggerFunD365WepApi.Core
{
    public class D365WebApiTokenDemo
    {
        private string _token;
        [FunctionName("D365WebApiTokenDemo")]
        public void Run([TimerTrigger("0 */1 * * * *")] TimerInfo myTimer, ILogger log)
        {
            log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
            var tokenResponse = GenerateToken();
            _token = tokenResponse.Result;
            var whoAmI = WhoAmI();
            log.LogInformation($"Who am I? {whoAmI.Result}");

        }

        public async Task<string> GenerateToken()
        {
            var organizationUrl = Environment.GetEnvironmentVariable("OrganizationUrl");
            var aadInstance = Environment.GetEnvironmentVariable("AadInstance");

            var applicationId = Environment.GetEnvironmentVariable("ApplicationId");
            var tenantId = Environment.GetEnvironmentVariable("TenantId");
            var clientSecret = Environment.GetEnvironmentVariable("ClientSecret");


            var clientCredentials = new ClientCredential(applicationId, clientSecret);
            var authenticationContext = new AuthenticationContext(aadInstance + tenantId);
            var authenticationResult = await authenticationContext.AcquireTokenAsync(organizationUrl, clientCredentials);

            return authenticationResult?.AccessToken ?? throw new InvalidOperationException("No Token Received");
        }

        public async Task<string> WhoAmI()
        {
            var crmWebApiBaseUrl = Environment.GetEnvironmentVariable("CrmWebApiBaseUrl");
            var httpClient = new HttpClient();
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
                _token);

            var whoAmIResponse = await httpClient.GetAsync($"{crmWebApiBaseUrl}/WhoAmI()");

            return await whoAmIResponse.Content.ReadAsStringAsync();
        }
    }
}

Tuesday 22 December 2020

RabbitMQ Trigger for Azure Functions

RabbitMQ is a popular message broker (https://www.rabbitmq.com/). Azure functions support RabbitMQ triggers and bindings. In other words, azure function can listen to a RabbitMQ message queue and also possible to push a message to the RabbitMQ message queue.

 

Microsoft Documentation and sample C#

https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-rabbitmq

https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-rabbitmq-trigger?tabs=csharp