Saturday, 25 October 2025

Configuring Azure Attachment Management and Azure Blob Storage

 1. Download Attachment Manager from Microsoft App Source and install it in your respective Orgs.

2. Connect to your CRM Instance

3. Create Azure Storage as shown I pic below:

Once created, open the storage account and scroll down and open the Blobsservice. Here, the admin will be able to create the containers that will contain the attachments uploaded via CRM. 

Create two global containers (which will be utilised in the Azure Attachment Storage Configuration page) and then create a container for each entity that will map attachments to Azure Blob Storage.

Note: Create two (global) containers named “emailsattachmentcontainer” and “notesattachmentcontainer.” Set the “Public Access Level” of any other containers for their respective entities to “Blob,” as this will enable users to have a preview feature

4.       Now provide access to the users by navigating to Shared Access Signature in Azure portal and create SAS Token by setting Expiry DateTime as shown below :

5.       Now navigate to the Dynamics Azure Attachments and paste SAS token in Configuration window as shown below :

6.       Now enable the Entity for Azure Blob Storage and map Entity in Container window as shown below :

7.       For Email Attachments , map Email entity as shown below :

8.       Provide necessary Access to Users by assigning Security Roles which has Azure Blob Storage Settings, NotesEntityAttachment Settings permissions.

Friday, 22 December 2023

How to Connect to Azure B2C using C#

 Below is the code related to how to connect to Azure AD B2C using B2CGraphClient. 


    1. Get Access Token using following approach : 

        

 this.clientId = clientId;

            this.clientSecret = clientSecret;

            this.tenant = tenant;


            // The AuthenticationContext is ADAL's primary class, in which you indicate the direcotry to use.

            this.authContext = new AuthenticationContext("https://login.microsoftonline.com/" + tenant);


            // The ClientCredential is where you pass in your client_id and client_secret, which are 

            // provided to Azure AD in order to receive an access_token using the app's identity.

            this.credential = new ClientCredential(clientId, clientSecret);


2.  Send Graph Request using below code : 


 // First, use ADAL to acquire a token using the app's identity (the credential)

            // The first parameter is the resource we want an access_token for; in this case, the Graph API.


            Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult result = await authContext.AcquireTokenAsync("https://graph.windows.net", credential);

            //AuthenticationResult result = authContext.AcquireToken("https://graph.windows.net", credential);


            // For B2C user managment, be sure to use the 1.6 Graph API version.

            HttpClient http = new HttpClient();

            string url = "https://graph.windows.net/" + tenant + api + "?" + Globals.aadGraphVersion;

            if (!string.IsNullOrEmpty(query))

            {

                url += "&" + query;

            }


            Console.ForegroundColor = ConsoleColor.Cyan;


            // Append the access token for the Graph API to the Authorization header of the request, using the Bearer scheme.

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);

            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            HttpResponseMessage response = await http.SendAsync(request);


            if (!response.IsSuccessStatusCode)

            {

                string error = await response.Content.ReadAsStringAsync();

                object formatted = JsonConvert.DeserializeObject(error);

                throw new WebException("Error Calling the Graph API: \n" + JsonConvert.SerializeObject(formatted, Formatting.Indented));

            }


            Console.ForegroundColor = ConsoleColor.Green;


            await response.Content.ReadAsStringAsync();

Send Email from Microsoft Dynamics CRM

 Below is the code reg how to send Email from D365 CRM using C# : 


  Entity email = new Entity("email");


        if (!string.IsNullOrEmpty(To))

        {

            EntityCollection toCollect = new EntityCollection();

            var partylist = NATo.Split(';');

            if (partylist.Length > 0)

            {

                foreach (var partys in partylist)

                {

                    if (!string.IsNullOrEmpty(partys))

                    {

                        Entity toparty = new Entity("activityparty");

                        toparty["addressused"] = partys;

                        toCollect.Entities.Add(toparty);

                    }

                }

            }

            else if (!string.IsNullOrEmpty(To))

            {

                Entity toparty = new Entity("activityparty");

                toparty["addressused"] = To;

                toCollect.Entities.Add(toparty);

            }


            email["to"] = toCollect;

        }


        string fetchportalconfigutaions = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>

                              <entity name='portalconfiguration'>

                                <all-attributes />

                                <filter type='and'>

                                  <condition attribute='name' operator='eq' value='SEND_EMAIL_ID'/>

                                </filter>

                              </entity>

                            </fetch>";

        EntityCollection emailfrom = service.RetrieveMultiple(new FetchExpression(fetchportalconfigutaions));

        string strEntityName = string.Empty, strRecordID = string.Empty;

        Guid FromID = Guid.Empty;

        if (emailfrom.Entities.Count > 0)

        {

            if (emailfrom[0].Attributes.Contains("NA_configurationkey"))

                strEntityName = emailfrom[0].Attributes["NA_configurationkey"].ToString();

            if (emailfrom[0].Attributes.Contains("NA_configurationvalue"))

                strRecordID = emailfrom[0].Attributes["NA_configurationvalue"].ToString();

        }


        Entity Fromparty = new Entity("activityparty");

        if (!string.IsNullOrEmpty(strEntityName) && !string.IsNullOrEmpty(strRecordID) && Guid.TryParse(strRecordID, out FromID))

            Fromparty["partyid"] = new EntityReference(strEntityName, FromID);

        else

        {

            Fromparty["partyid"] = new EntityReference("systemuser", new Guid("638A9F0A-DFB2-E711-A824-000D3AA32A16"));

        }


        email["from"] = new Entity[] { Fromparty };

        email["subject"] = subject;


        if (string.IsNullOrWhiteSpace(bodyxml) || string.IsNullOrEmpty(bodyxml))

        {

            email["description"] = bodyHTML;

        }

        else if (!string.IsNullOrEmpty(bodyxml) || !string.IsNullOrWhiteSpace(bodyxml))

        {

            email["description"] = bodyxml;

        }


        email["directioncode"] = true;

        email["regardingobjectid"] = PostImage.ToEntityReference();


        Guid emailId = service.Create(email);


        if (emailId != null && emailId != Guid.Empty)

        {

            tracingService.Trace("Email Activity Created. ID:-" + Convert.ToString(emailId));


            tracingService.Trace("Wait 5 Seconds before adding attahment");

            System.Threading.Thread.Sleep(new TimeSpan(0, 0, 5));

            tracingService.Trace("Continuing :  Adding attahment");

            QueryExpression queryNotes = new QueryExpression("annotation");

            queryNotes.ColumnSet = new ColumnSet(new string[] { "subject", "mimetype", "filename", "documentbody" });

            queryNotes.Criteria = new FilterExpression();

            queryNotes.Criteria.FilterOperator = LogicalOperator.And;

            queryNotes.Criteria.AddCondition(new ConditionExpression("objectid", ConditionOperator.Equal, PostImage.ToEntityReference().Id));

            EntityCollection mimeCollection = service.RetrieveMultiple(queryNotes);

            foreach (var attachment in mimeCollection.Entities)

            {

                Entity emailAttachment = new Entity("activitymimeattachment");

                if (attachment.Contains("subject")) emailAttachment["subject"] = attachment["subject"];

                if (attachment.Contains("filename")) emailAttachment["filename"] = attachment["filename"];

                if (attachment.Contains("documentbody")) emailAttachment["body"] = attachment["documentbody"];

                if (attachment.Contains("mimetype")) emailAttachment["mimetype"] = attachment["mimetype"];

                emailAttachment["objectid"] = new EntityReference("email", emailId);

                emailAttachment["objecttypecode"] = "email";

                service.Create(emailAttachment);

                tracingService.Trace("Email Attachment Added.");

            }


            SendEmailRequest sendEmailreq = new SendEmailRequest

            {

                EmailId = emailId,

                IssueSend = true

            };


            SendEmailResponse sendEmailresp = (SendEmailResponse)service.Execute(sendEmailreq);

        }

How To Retrieve More than 5K Records in MS CRM

  Please find below sample code to check how to retrieve more than 5K Records in MS CRM using C# code :


Problem Statement : Query Expression can’t fetch more than 5k Records in Ms CRM

Create a Paging Programmatically and Execute data present in first page. Then proceed with other pages until execution completes for all Number of Records in Production.

C# Code to achieve Solution  using Query Expression :

  string countryLocale = "%" + countryId + "%";

                QueryExpression query = new QueryExpression("customerproductwarranty");

               

                // Or retrieve All Columns

                query.ColumnSet = new ColumnSet(true);

                ConditionExpression condition1 = new ConditionExpression();

                condition1.AttributeName = "locale";

                condition1.Operator = ConditionOperator.Like;

                condition1.Values.Add(countryLocale);

                FilterExpression filter1 = new FilterExpression();

                filter1.Conditions.Add(condition1);

                query.Criteria.AddFilter(filter1);

 

                query.PageInfo = new PagingInfo();

                query.PageInfo.Count = 1000;

                query.PageInfo.PageNumber = 1;

                query.PageInfo.ReturnTotalRecordCount = true;

                EntityCollection myContacts = service.RetrieveMultiple(query);

 

                Console.WriteLine("First set of Records retrieved are :" + myContacts.Entities.Count);

                objErrorLog.WriteErrorLog("First set of Records retrieved are :"+myContacts.Entities.Count);

                var queryResult = service.RetrieveMultiple(query);

                if (queryResult.Entities.Count > 0)

                {

                    count = customerWarrantyDateFormatFix(queryResult , i , service);

                    i += count;

                }

               while (myContacts.MoreRecords)

                {

                    query.PageInfo.PageNumber += 1;

                    query.PageInfo.PagingCookie = myContacts.PagingCookie;

                    myContacts = service.RetrieveMultiple(query);

                    Console.WriteLine("More Records retrieved are : " + myContacts.Entities.Count);

                    objErrorLog.WriteErrorLog("More Records retrieved are :" + myContacts.Entities.Count);

                    //Add to the collection

                    var queryResult1 = service.RetrieveMultiple(query);

                    if (queryResult1.Entities.Count > 0)

                    {               

                       count = customerWarrantyDateFormatFix(queryResult1 ,i, service);

                        i = count;

                    }

                                    }

             Place your functionalities inside method : customerWarrantyDateFormatFix so that Operations like Update / Get can be performed.

 

C# code to achieve using Fetch XML : 


string countryLocale = "%" + countryId + "%";

// int recordCreatedBeforeDays = createdBeforeDays;

// Define the fetch attributes.

// Set the number of records per page to retrieve.

int fetchCount = 5000;

// Initialize the page number.

int pageNumber = 1;

// Initialize the number of records.

int recordCount = 0;

// Specify the current paging cookie. For retrieving the first page, 

// pagingCookie should be null.

string pagingCookie = null;

            try

            {


String fetchRecordsfromCRMforNonAutoWarrantyCountries = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>

                      <entity name='contact'>

                       <all-attributes />

                        <order attribute='fullname' descending='false' />

                        <filter type='and'>

                          <condition attribute='locale' operator='like' value='" + countryLocale + @"' />

                          <condition attribute = 'emailaddress1' operator= 'not-null'  /> 

                          <condition attribute='modifiedon' value='" + lastXDays + @"' operator='last-x-days'/>

                        </filter>

                      </entity>

                    </fetch>";


                while (true)

                {

                    string xml = string.Empty;


                         xml = CreateXml(fetchRecordsfromCRMforNonAutoWarrantyCountries, pagingCookie, pageNumber, fetchCount);

 

                    // Excute the fetch query and get the xml result.

                    RetrieveMultipleRequest fetchRequest1 = new RetrieveMultipleRequest

                    {

                        Query = new FetchExpression(xml)

                    };


EntityCollection returnCollection = ((RetrieveMultipleResponse)service.Execute(fetchRequest1)).EntityCollection;

                    

}

                    // Check for morerecords, if it returns 1.

                    if (returnCollection.MoreRecords)

                    {

                        Console.WriteLine("\n****************\nPage number {0}\n****************", pageNumber);

                        objErrorLog.WriteErrorLog("\n****************\nPage number {0}\n****************", pageNumber.ToString());


                        // Increment the page number to retrieve the next page.

                        pageNumber++;

                        //refresh the connection

                        service = (IOrganizationService) conn.OrganizationWebProxyClient ?? (IOrganizationService) conn.OrganizationServiceProxy;

objErrorLog.WriteErrorLog("connection refreshed");

                        Console.WriteLine("Connection refreshed");

                        // Set the paging cookie to the paging cookie returned from current results.                            

                        pagingCookie = returnCollection.PagingCookie;

                    }

                    else

                    {

                        // If no more records in the result nodes, exit the loop.

                        break;

                    }

    }

            catch (Exception e)

            {

                Console.WriteLine(" No Records fetched for given Query  ");

                objErrorLog.WriteErrorLog(" No Records fetched with Exception  " + e);

            }



Azure Key Vault Connection to connect to MS CRM

 Below is the sample code regarding how to connect to Azure Key Vault to get Credentials to connect to any Ms CRM Organizations 


 // Azure Key Vault Connections 

            string keyVaultName = ConfigurationManager.AppSettings["KeyVaultName"];

            string crmUrl = ConfigurationManager.AppSettings["crmUrl"];


            string password = "";

            var kvUri = String.Format(ConfigurationManager.AppSettings["KeyVaultUrl"], keyVaultName);


            var credential = new Azure.Identity.ClientSecretCredential(ConfigurationManager.AppSettings["KeyVaultAzureTenantID"], ConfigurationManager.AppSettings["KeyVaultAzureClientID"], ConfigurationManager.AppSettings["KeyVaultAzureClientSecret"]);

            var client = new SecretClient(new Uri(kvUri), credential);


            var user = client.GetSecret(ConfigurationManager.AppSettings["KeyValueUserIDSecret"]).Value;

            userName = user.Value;


            var pwd = client.GetSecret(ConfigurationManager.AppSettings["KeyValuePasswordSecret"]).Value;

            password = pwd.Value;


            string connectionString = string.Format("AuthType=OAuth;Username={0};Password={1};Url={2};AppId=760c6b53-9d70-4fe0-8b08-2b6001f7069d;RedirectUri=app://468550f1-718b-416c-bd12-a686589aadbb;LoginPrompt=Never;RequireNewInstance = True", userName, password, crmUrl);


            var conn = new CrmServiceClient(connectionString);


IOrganizationService service;

            service = (IOrganizationService)conn.OrganizationWebProxyClient ?? (IOrganizationService)conn.OrganizationServiceProxy;


Tuesday, 2 August 2022

Dynamics Portal Web Api

     You can use the Web API to perform create, read, update, and delete operations across all Microsoft Dataverse tables from your portals pages. Below set of Operations can be performed using Portal Web Apis : 

    

  • Read records from a table
  • Create a record in a table
  • Update and delete records in a table

  • Associate and disassociate tables

Design Flow : 



Steps to enable Portal Web Api in your CRM Organization : 

1. Create Site setting for Web Api to use particular Entity as shown below :

    > Site Settings > 


2. Create another site setting for the Entity fields to use as shown below : 

                  

3. Enable Entity Permissions for the Contact by using Table permissions setting and provide add newly created  Web Role to provide access to your web api 

4. Once all the above steps are completed - you can update your contact Record from portal pages using below code with the help of Portal Web api as shown below : 

(function(webapi, $){
function safeAjax(ajaxOptions) {
var deferredAjax = $.Deferred();
shell.getTokenDeferred().done(function (token) {
// add headers for ajax
if (!ajaxOptions.headers) {
$.extend(ajaxOptions, {
headers: {
"__RequestVerificationToken": token
}
}); 
} else {
ajaxOptions.headers["__RequestVerificationToken"] = token;
}
$.ajax(ajaxOptions)
.done(function(data, textStatus, jqXHR) {
validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
}).fail(deferredAjax.reject); //ajax
}).fail(function () {
deferredAjax.rejectWith(this, arguments); // on token failure, pass the token ajax and args
});
return deferredAjax.promise();
}
webapi.safeAjax = safeAjax;
})(window.webapi = window.webapi || {}, jQuery)



// Notification component
var notificationMsg = (function() {
var $processingMsgEl = $('#processingMsg'),
_msg = 'Processing...',
_stack = 0,
_endTimeout;
return {
show: function(msg) {
$processingMsgEl.text(msg || _msg);
if (_stack === 0) {
clearTimeout(_endTimeout);
$processingMsgEl.show();
}
_stack++;
},
hide: function() {
_stack--;
if (_stack <= 0) {
_stack = 0;
clearTimeout(_endTimeout);
_endTimeout = setTimeout(function() {
$processingMsgEl.hide();
}, 500);
}
}
}
})();

//Applicaton ajax wrapper 
function appAjax(processingMsg, ajaxOptions) {
notificationMsg.show(processingMsg);
return webapi.safeAjax(ajaxOptions)
.fail(function(response) {
if (response.responseJSON) {
alert("Error: " + response.responseJSON.error.message)
} else {
alert("Error: Web API is not available... ")
}
}).always(notificationMsg.hide);
}

var userGuid = $("#adx_user_guid").val();
     appAjax('Updating...', {
      type: "PATCH",
url: "/_api/contacts("+userGuid+")",
contentType: "application/json",
data: JSON.stringify({
" Login Time": dateTime,
}),
success: function (res) {
console.log(res);
}
        });


The above code should update your contact record with Logged in user Date time. Hope it helps. 

Thank You!



Wednesday, 21 July 2021

 

Get/Post/Delete Requests to Azure AD B2C

 

                     


 

As Azure AD B2C is growing to replace Authentication methodology for Microsoft Dynamics Portals as well as other Applications , there is a growing demand to handle Get/Post/Delete Requests  to Azure AD B2C via C# .Net Code.

 

Below is the Info regarding how to make API Requests to Azure B2C using simple coding approaches and Details reg Pre-release Versions.

Dll\Namespace Required to Acquire Classes to access Microsoft Graph APIs :

1.       Microsoft.Graph

2.       Microsoft.Identity.Client

 

 

 

Above mentioned dlls contain definition for IConfidentialClientApplication which can be used to get AuthProvider as shown below :

 

     IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder

            .Create(clientId)

            .WithTenantId(tenant)

            .WithClientSecret(clientSecret)

            .Build();

 

                  ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

Problem :

ClientCredentialProvider method present in Base  Microsoft.Identity.Client Dll and it is not completely tested and recommended by Microsoft yet as it is still in PreRelease stage and we can’t use it in Production. Also , Visual Studio doesnot recognise ClientCredentialProvider class. Hence, we can proceed with below approach which provides authContext and also helps in performing Get/Delete Requests to Azure AD B2C.

 

Solution :

Set below Configurations in App.Config :

            string clientId = ConfigurationManager.AppSettings["b2c:ClientId"];

            string clientSecret = ConfigurationManager.AppSettings["b2c:ClientSecret"];

            string tenant = ConfigurationManager.AppSettings["b2c:Tenant"];

 

      // The app registration should be configured to require access to permissions

        // sufficient for the Microsoft Graph API calls the app will be making, and

            // those permissions should be granted by a tenant administrator.

            var scopes = new string[] { "https://graph.microsoft.com/.default" };

            // Configure the MSAL client as a confidential client

            var confidentialClient = ConfidentialClientApplicationBuilder

                .Create(clientId)

                .WithAuthority($"https://login.microsoftonline.com/$tenantId/v2.0")

                .WithClientSecret(clientSecret)

                .Build();

 

            // Build the Microsoft Graph client. As the authentication provider, set an async lambda

            // which uses the MSAL client to obtain an app-only access token to Microsoft Graph,

            // and inserts this access token in the Authorization header of each API request.

            GraphServiceClient graphServiceClient =

                new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) => {

 

                    // Retrieve an access token for Microsoft Graph (gets a fresh token if needed).

                    var authResult = await confidentialClient

                        .AcquireTokenForClient(scopes)

                        .ExecuteAsync();

 

                    // Add the access token in the Authorization header of the API request.

                    requestMessage.Headers.Authorization =

                        new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

                })

                );

 

Make Either Get Request to Azure B2C or Delete Requests as shown below :

 

            // Make a Microsoft Graph API query

            graphServiceClient.Users[B2cObjectId/User-Id]

               .Request()

               .DeleteAsync();

        }

 

This helps us to fetch Client Id , Client Secret and Tenant Id from App Settings and Get Token as required and perform necessary operations using simple Requests.