Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Sep 12, 2025

Attach PDF file programmatically to Email and send

Let's see how PDF file is attached to an Email and sending programmatically. Lets see why this is important.

1) Email Templates cannot be created with an attachment. (Ironically!)
2) If we attach programmatically we can change the attachment based on logic if required.

In Summery, we can initiate Email in Draft state, then attach the PDF and then send.

Initiation and sending the e-mail part is explained in Programmatically create a draft Email using Email Template (C#).

Lets see the code

// Initiate Email in Draft state

// Create Attachment
byte[] embededPdf = LoadEmbeddedPdf("PluginProject.Resources.ApplicationForm.pdf")
Guid attachmentId = CreatePdfAttchment(OrgSvc,emailId, Convert.ToBase64String(embededPdf));

// Send Email

// Methods to use
private byte[] LoadEmbeddedPdf(string resourceName)
{
    var assembly = Assembly.GetExecutingAssembly(); 
    using (Stream stream = assembly.GetManifestResourceStream(resourceName))
    {
        if (stream == null)
            throw new FileNotFoundException("Embedded PDF file not found: " + resourceName);
        using (MemoryStream ms = new MemoryStream())
        {
            stream.CopyTo(ms);
            return ms.ToArray();
        }
    }
}

public Guid CreatePdfAttchment(IOrganizationService OrgSvc, Guid emailId, string base64Pdf)
{
    Entity attachment = new Entity("activitymimeattachment");
    attachment["subject"] = "Application Form";
    attachment["filename"] = "ApplicationForm.pdf";
    attachment["mimetype"] = "application/pdf";
    attachment["body"] = base64Pdf;
    attachment["attachmentnumber"] = 1;
    attachment["objectid"] = new EntityReference("email", emailId);
    attachment["objecttypecode"] = "email";
    return OrgSvc.Create(attachment);
}

There are two important things here;

1. PDF is saved within the Project and path needs to be passed correctly in below standard.

<Project Namespace><Folder><filename>.pdf

So you will realize when I pass PluginProject.Resources.ApplicationForm.pdf, my project namespace is  PluginProject, folder name is Resources and pdf file name is ApplicationForm.pdf.

2. Go to properties of the PDF file (In VS) and do below setting.
    


Mar 14, 2025

Send Email with dynamic excel attachment

Sending an email with associated excel sheet with details of child records is a generic requirement and also great way to pass child record details to a customer. For example, you may need to send an email to a customer with quote details and associate quote products as a excel attachment. 

In my example I am sending email to a Account and Sender is a Queue (you may obviously use a System user). I am sending Opportunity details and associated excel will carry Opportunity Products. This Email will be shown in timeline of Opportunity hence regarding Object would be the Opportunity. 

I have two separate code snippets here.

1) Sending the Email with Template and Excel

What we need to understand is we should create the Email first and then send as two steps.. This way, we get a room prior to sending, to attach the excel. Note how we pass different Ids like Sender, Template, Receiver and Regarding Object here. Also notice that we pass the excel as the attachment body.

Public void SendEmailWithPaymentEvaluationDetails(Guid templateId, Guid toAccountId, Guid fromQueueId, Guid regOpportunityId)
{
    // Initiate Email
    InstantiateTemplateRequest request = new InstantiateTemplateRequest()
    {
        TemplateId = new Guid(templateId),
        ObjectId = invoiceRequest.Id,
        ObjectType = invoiceRequest.LogicalName
    };
    InstantiateTemplateResponse response = (InstantiateTemplateResponse)OrgService.Execute(request);
    Entity email = response.EntityCollection[0];
    Entity Fromparty = new Entity("activityparty");
    Entity Toparty = new Entity("activityparty");
    Toparty["partyid"] = new EntityReference("account", toAccountId);
    Fromparty["partyid"] = new EntityReference("queue", fromQueueId);
    email["from"] = new Entity[] { Fromparty };
    email["to"] = new Entity[] { Toparty };
    email["directioncode"] = true;
    email["regardingobjectid"] = new EntityReference("opportunity", regOpportunityId);
    Guid emailId = OrgService.Create(email);

    // Link the Attachment
    Entity attachment = new Entity("activitymimeattachment");
    attachment["subject"] = "OpportunityId Product List";
    attachment["filename"] = "OpportunityId Product List.xlsx";
    attachment["body"] = Convert.ToBase64String(CompileExcelFile(regOpportunityId));
    attachment["mimetype"] = "application/vnd.ms-excel";
    attachment["attachmentnumber"] = 1;
    attachment["objectid"] = new EntityReference(email.LogicalName, emailId);
    attachment["objecttypecode"] = email.LogicalName;
    OrgService.Create(attachment);

    // Send Email
    SendEmailRequest sendEmailRequest = new SendEmailRequest
    {
        EmailId = emailId,
        TrackingToken = string.Empty,
        IssueSend = true
    };
    SendEmailResponse sendEmailResponse = (SendEmailResponse)OrgService.Execute(sendEmailRequest);
}

2) Compilation of Excel

Here we compile the excel which is called in above method when preparing the attachment. 
In this technique, we need to have a view created in Opportunity Product and retrieve its Id to be used here. That's the Id you see in SavedQuery. Interestingly, though we assign an id of a saved view, we are defining dynamically what we need in the excel via our own Fetch query. Under grid section, we adjust the column widths etc.
 
public byte[] CompileExcelFile(Guid regOpportunityId)
{
    var exportToExcelRequest = new OrganizationRequest("ExportToExcel");
    exportToExcelRequest.Parameters = new ParameterCollection();
    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("View", new EntityReference("savedquery", new Guid("{4c523f5b-e8c5-4cb5-bc83-bf4ef934342d}"))));
    string stringFetchXml = @"<fetch distinct='false' no-lock='false' mapping='logical' returntotalrecordcount='true'>
                                <entity name='opportunityproduct'>
                                    <attribute name='lineitemnumber' />
                                    <attribute name='productname' />
                                    <attribute name='description' />
                                    <attribute name='baseamount' />
                                    <filter>
                                           <condition attribute='opportunityid' operator='eq' value='{0}' />
                                    </filter>
                                   </entity>
                            </fetch>";
    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("FetchXml", String.Format(stringFetchXml, regOpportunityId.ToString())));
    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("LayoutXml", @"
            <grid name='resultset' object='2' jump='lineitemnumber' select='1' icon='1' preview='1'>
                <row name='result' id='opportunityproductid'>
                    <cell name='lineitemnumber' width='100' />
                    <cell name='productname' width='200' />
                    <cell name='description' width='300' />
                    <cell name='baseamount' width='125' />
                </row>
            </grid>"));
    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("QueryApi", ""));
    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("QueryParameters", new InputArgumentCollection()));
    var exportToExcelResponse = OrgService.Execute(exportToExcelRequest);
    if (exportToExcelResponse.Results.Any())
        return exportToExcelResponse.Results["ExcelFile"] as byte[];
    else
        return null;
}

Hope this helps!

Feb 14, 2025

Programmatically create a draft Email using Email Template (C#)

Previously we discussed how to send an Email using a Email Template, but we noticed it just sends the email but no chance of create and save as a draft. In some instances we need to create the Draft email to be sent later after checking or/and modifications by the user. 

In such situations we can use below code. One can say its simply possible to use CREATE message of the Org service to achieve this but it is not possible to use a Template which is a constrain. 

By creating the draft first, it allows you to programmatically or manually attach attachments prior to sending the email.

Here InstantiateTemplateRequest  message does the magic.

 public void CreateDraftEmailToPrimaryContactOfAccount(Account account)
 {
     InstantiateTemplateRequest request = new InstantiateTemplateRequest()
     {
         TemplateId = new Guid("bf0b97c7-d5a3-4a3f-8771-a1cd737ab555"),
         ObjectId = account.Id,
         ObjectType = Account.LogicalName
     };
     InstantiateTemplateResponse response = (InstantiateTemplateResponse)OrgService.Execute(request);

     Entity email = response.EntityCollection[0];
     Entity Fromparty = new Entity("activityparty");
     Entity Toparty = new Entity("activityparty");
     Toparty["partyid"] = new EntityReference(Account.EntityLogicalName, account.PrimaryContactId.Id);
     Fromparty["partyid"] = new EntityReference("queue", new Guid("f14a45e9-fac5-4ba0-9a95-c07fe1adabf0"));
     email["from"] = new Entity[] { Fromparty };
     email["to"] = new Entity[] { Toparty };
     email["directioncode"] = true;
     email["regardingobjectid"] = new EntityReference(Account.EntityLogicalName, account.Id);
     Guid emailId = OrgService.Create(email);
}

When ready if you need to send the Email programmatically, below SendEmailRequest message can be used as below.

SendEmailRequest sendEmailRequest = new SendEmailRequest
{
     EmailId = emailId,
     TrackingToken = string.Empty,
     IssueSend = true
};
SendEmailResponse sendEmailResponse = (SendEmailResponse)OrgService.Execute(sendEmailRequest);

Related Posts
Programmatically send Email with Template (c#)

Programmatically send Email with Template (c#)

This is a code snippet that sends Email programmatically (in C#) while using an Email Template

Explained scenario, we send an Email to Primary Contact of the Account. We set Account as the regarding object of the Email so this Email activity will associate with Account and will be displayed in Account's timeline. Also we have to pass Account as the regarding object of Template since we want dynamic fields of Template to be filled with Account field values as necessary.

Actually SendEmailFromTemplateRequest message does the magic.

public void SendEmailToPrimaryContactOfAccount(Account account)
{
    Entity Fromparty = new Entity("activityparty");
    Entity Toparty = new Entity("activityparty");
    Toparty["partyid"] = new EntityReference(Contact.EntityLogicalName, account.PrimaryContactId.Id);
    Fromparty["partyid"] = new EntityReference("queue", new Guid("f14a45e9-fac5-4ba0-9a95-c07fe1adabf0"));
    Entity email = new Entity("email");
    email["from"] = new Entity[] { Fromparty };
    email["to"] = new Entity[] { Toparty };
    email["directioncode"] = true;
    email["regardingobjectid"] = new EntityReference(Account.EntityLogicalName, account.Id);

    SendEmailFromTemplateRequest emailUsingTemplateReq = new SendEmailFromTemplateRequest
    {
        Target = email,
        TemplateId = new Guid("bf0b97c7-d5a3-4a3f-8771-a1cd737ab555"),
        RegardingId = account.Id,
        RegardingType = Account.EntityLogicalName
    };
    var emailUsingTemplateRes = OrgService.Execute(emailUsingTemplateReq);
} 

Please note I am sending this email from a queue record. Obviously you can send from a system users as well. In such case, from activity party has to be changed accordingly.

One thing to note is, this method just sends the email, so you are not save Email in draft status.

Related Posts
Programmatically create a draft Email using Email Template (C#)

May 17, 2024

Execute Multiple

In some cases we need to repeat the same operation. It may be creating a collection of records or updating a collection or records. We are good to do those via available CRUD operations of the Organization Service.

However if we do a collection within a loop, we should understand we repeat the same operation that result number of server calls which is not sufficient. Particularly, if you need to do this within a synchronous process it important to complete quickly rather keeping the user waiting.

In such situations, we would do execute multiple operations. Lets check the code snippet.

var executeMultipleRequest = new ExecuteMultipleRequest
{
    Settings = new ExecuteMultipleSettings { ContinueOnError = true, ReturnResponses = false },
    Requests = new OrganizationRequestCollection()
};

for (int i = 0; i < 5; i++)
{
    var paymentevaluation = new Entity("contact")
    {
        ["firstname"] = $"Test First Name {i}",
        ["lastname"] = $"Test Last Name {i}",
    };

    executeMultipleRequest.Requests.Add(new CreateRequest { Target = paymentevaluation });
}

var data = (ExecuteMultipleResponse)service.Execute(executeMultipleRequest);

foreach (var response in data.Responses)
{
    Console.WriteLine($"{response.RequestIndex}: {response.Fault}..");
}
Notice that we add all the entities to be created and execute once in this code. This is significantly faster than going through a loop.
Notes
> Keep in mind, maximum number of requests can be added to a execution is 1000.
> This also had a limitation of 2 concurrent operations, but now its removed.

May 11, 2024

ConditionOperator "Contains" doesnt work

When retrieving data using RetrieveMultiple we use different condition operators apart from Equal which is the most used one. 

Anyway, I noticed Contains operator is not working. For example below code, though looks alright, will not work. Its disappoint error message is not helpful either.

   
var query = new QueryExpression("account")
{
    ColumnSet = new ColumnSet("accountid", "name"),
    Criteria = new FilterExpression(LogicalOperator.And),
    TopCount = 10
};
query.Criteria.AddCondition("so_groupname", ConditionOperator.Contains, "expolanka");
Solution is to you like key work instead of Contains which does the same. Anyway, one thing to keep in mind is you need to use % mark for the task your check for. Check below;
   
query.Criteria.AddCondition("so_groupname", ConditionOperator.Like, "%expolanka%");

Oct 18, 2023

Write a Custom API and call from Workflow

A Custom API is comparatively a new mechanism to create your own API in Dataverse. This is like a Action where you can package few steps to accomplish something. Advantage is you can then call from anywhere, either client side or server side etc. Obviously it can be called via a workflow or Cloud Flow too. Custom API is more flexible since coding is possible.

In this scenario we will see how to create a simple Custom API and call it from a classic workflow.

Writing a Custom API

1) We need to write a code which is pretty similar to a Plug-in. This is where we define our logic. In my case I am accepting a parameter string (i.e. DetailStr). Logic is to update Description (i.e. so_description) field of the custom entity called Office (i.e. so_office) with parameter value.

namespace AccAdmin.Plugins.CustomApi
{
    public class UpdateOffice : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context;
            IOrganizationServiceFactory factory;
            IOrganizationService service;

            try
            {
                context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
                ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

                if (!context.InputParameters.Contains("DetailStr"))
                    throw new InvalidPluginExecutionException($"DetailStr not designated or Invalid..");

                if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is EntityReference target)
                {
                    if (target.LogicalName != "so_office") return;
                    factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    service = factory.CreateOrganizationService(context.UserId);

                    Entity entity = new Entity();
                    entity.LogicalName = "so_office";
                    entity.Id = context.PrimaryEntityId;
                    entity["so_description"] = (string)context.InputParameters["DetailStr"];
                    service.Update(entity);
                }
            }
            catch (FaultException<OrganizationServiceFault> e)
            {
                throw e;
            }
            finally
            {
                service = null;
                factory = null;
                context = null;
            }
        }
    }
}

2) Once register the Plug-in, Custom API step will be visible as a step as below.


3) Now we start configuration. First add new Custom API to your solution.
    

Once save my Custom API was seen like below. It will have an unique name which we use to call this. We have set Enable for Workflow = Yes since we plan to call this from a Workflow. Most importantly, we have set our Plug-in step as the Plugin Type which is the linking point of our logic to the API. 


* Idea of all other settings

4) Same way we added the Custom API, we now add a parameter to our solution. Parameter is optional. In our case we plan to pass a string parameter.


Once created, parameter was seen as below. Nothing complex about this. You may notice we have selected Custom API we defined in previous step is set here for Custom API field.


Now we are done with our Custom API

Calling the Custom API from Workflow

Here I am create just an On Demand workflow so we can easily check it.

5) I am initiating a very simple Async workflow against Office entity.


6) Now I am adding a step of performing an Action. Interestingly, when I check the list of available Actions, I am seeing my Custom API by its unique name.


I see two parameters here. One is Target where we need to pass the current office record. Other one is what we define in Custom API which is needed for our logic.


7) Now its a matter of running our Workflow On Demand for any selected Office record.

* 
Unique Name: Unique name for the Custom API (Start with the publisher’s prefix which you have specified on your solution).

Name: Name for the Custom API.

Display Name: Display Name for this Custom API. In case we have enabled multi-languages on the System, we can provide a specific name for the custom API based on the language.

Description: This field is to store the description of this Custom API.

Binding Type: The Binding Type is the Options set field and could be set as Global, Entity, and Entity Collection. This should be specified as per your operation requirement (Entity Collection is only supported in Function Type Custom API).

Bound Entity Logical Name: This field is required when we select the Binding Type as Entity or Entity Collection. For the binding type as Global, it can be empty.

Is Function: It defines whether your custom API is a Function (this can only be called using Get Method) or Action (this can only be called using POST method).

Is Private: To define the Custom API is private or public (setting it private makes it accessible to only the owner of the Custom API).

Allowed Custom Processing Step Type: Allowed Custom Processing Step Type is another Option set field with options like None, Async Only, Sync, and Async. These options let you define whether the other plugins could be registered on this Custom Message and also lets you define its behavior. Like Async Only allows the plugins to register only onPost operation with Execution Mode to Asynchronous, and same for the Sync and Async option.

Execute Privilege Name: We can define the privilege that allows execution of the custom API. As per Microsoft docs, we can also create custom privileges and it is currently in development. We can use OOB privileges, for example,prvCreateLead, prvWriteLead, etc.

Plugin Type: Set the reference of your plugin for this API.

Warning
This example works fine. Anyway, there has been some deployment issues reported as per now we write this. So it is always advisable to call your Custom API from a Cloud Flow than a classic workflow. That worked perfectly.

Aug 16, 2022

Retrieve instance URL from Custom Workflow Activity

When this requirement arises, I though it should be a matter of reading it from context or so on. Anyway, then I realized it’s not available and no straightforward way of doing it. So I would suggest below two ways to do that based on your circumstance. 

If your system has a separate entity for configuration data, like key value pairs, its best to store there. Advantage is this entry could be accessed from many other areas as needed. Since its store as data, deployments don’t override. 

If you really want to retrieve dynamically, there is one other way.

IWorkflowContext context = ExecutionContext.GetExtension<IWorkflowContext>();
context.OrganizationName

This attribute gives you the unique name of the instance. 

While this is unique to instance, you are able to write a case statement etc. to retrieve the correct URL. This is good because your system will switch dynamically to correct URL but you are keeping URLs in the code itself. This means if you add new environment you need to modify the code and re-deploy the assembly.

Sep 27, 2020

Handling the 5000 limit of FetchXml retrievals

When we retrieve records with FetchXml, most annoying constrain is limit of records it returns which is 5000. Here, we are checking how to overcome this limitation.

Paging solution

If you browse the web, you will find a lot of solutions using paging. Which is pretty cool. I am showing here one of the good codes I tried. This worked for me. For example I am retrieving all the active contacts in my CRM.

internal static void MainFunction(IOrganizationService service)
        {
            string fetchXml = string.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
                                        <entity name='contact' >
                                            <attribute name='firstname' />
                                            <attribute name='lastname' />
                                            <filter>
                                                <condition attribute='statecode' operator='eq' value='0' />
                                            </filter>
                                        </entity>
                                       </fetch>");

            List<Entity> contacts = GetTotalRecordsfromFetch(fetchXml, service);
            Console.WriteLine("contacts : " + contacts.Count);
         }
        
        public static List<Entity> GetTotalRecordsfromFetch(string fetchXML, IOrganizationService orgService)
        {
            List<Entity> lstEntity = new List<Entity>();
            int fetchCount = 5000;
            int pageNumber = 1;
            string pagingCookie = null;

            while (true)
            {
                string xml = CreateXml(fetchXML, pagingCookie, pageNumber, fetchCount);
                RetrieveMultipleRequest fetchRequest = new RetrieveMultipleRequest
                {
                    Query = new FetchExpression(xml)
                };

                var returnCollections = ((RetrieveMultipleResponse)orgService.Execute(fetchRequest)).EntityCollection;

                if (returnCollections.Entities.Count >= 1)
                {
                    lstEntity.AddRange(returnCollections.Entities);
                }

                if (returnCollections.MoreRecords)
                {
                    pageNumber++;

                    pagingCookie = returnCollections.PagingCookie;
                }
                else
                {
                    break;
                }
            }
            return lstEntity;
        }

        public static string CreateXml(string xml, string cookie, int page, int count)
        {
            StringReader stringReader = new StringReader(xml);
            XmlTextReader reader = new XmlTextReader(stringReader);

            XmlDocument doc = new XmlDocument();
            doc.Load(reader);

            XmlAttributeCollection attrs = doc.DocumentElement.Attributes;

            if (cookie != null)
            {
                XmlAttribute pagingAttr = doc.CreateAttribute("paging-cookie");
                pagingAttr.Value = cookie;
                attrs.Append(pagingAttr);
            }

            XmlAttribute pageAttr = doc.CreateAttribute("page");
            pageAttr.Value = System.Convert.ToString(page);
            attrs.Append(pageAttr);

            XmlAttribute countAttr = doc.CreateAttribute("count");
            countAttr.Value = System.Convert.ToString(count);
            attrs.Append(countAttr);

            StringBuilder sb = new StringBuilder(1024);
            StringWriter stringWriter = new StringWriter(sb);

            XmlTextWriter writer = new XmlTextWriter(stringWriter);
            doc.WriteTo(writer);
            writer.Close();

            return sb.ToString();
        }

Problem with Aggregate

Above solution doesn't work when u need to Aggregate records in FetchXml. For example I need to run below FetchXml to get count of Accounts group by Primary Contacts. Paging doesn't work here.

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" aggregate="true" >
  <entity name="account" >
    <attribute name="accountid" alias="AccountId" aggregate="count" />
    <attribute name="primarycontactid" alias="PrimaryContactid" groupby="true" />
    <filter>
      <condition attribute="statecode" operator="eq" value="0" />
    </filter>
  </entity>
</fetch>

At last, what I had to do was, not so pretty, to retrieve values by chunks. For example, below I am looping though the alphabet so that I am executing the fetch 26 times for each letter where contact name is starting from particular letter. 

internal static void MainFunction(IOrganizationService service)
        {
            const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            foreach (char ch in alphabet)
            {
                string fetchXml = string.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false' aggregate='true' >
                                      <entity name='account' >
                                        <attribute name='accountid' alias='AccountId' aggregate='count' />
                                        <attribute name='primarycontactid' alias='PrimaryContactid' groupby='true' />
                                        <filter>
                                          <condition attribute='statecode' operator='eq' value='0' />
                                          <condition attribute='primarycontactidname' operator='like' value='{{0}}' />
                                        </filter>
                                      </entity>
                                    </fetch>");
                string fetchXmlCompiles = string.Format(fetchXml, ch + "%");

                EntityCollection accounts = service.RetrieveMultiple(new FetchExpression(fetchXmlCompiles));
                Console.WriteLine(accounts.Entities.Count);
            }
            Console.ReadLine();
         }

This is not the best, yet I don't know better way of doing this. If you find one, pl don't forget to comment below.

Reference;

Oct 8, 2019

Passing Unsecure Configurations to a Plugin

We discussed how to pass Configurations to a JavaScript in a previous post. Click this to read that.

Lets see how we can pass Configurations to a Plug-in. In this scenarios, we'll see how we pass some Key Value pairs to a plug-in written for Lead entity.. We are going to store these values in a XML format within Unsecure Configuration section in registered plugin step.


This is the XML data format.

<leadConfig>
  <setting name="RegionCode" value="000X23AA55" />
  <setting name="IntegrationKey" value="1200-6753-0980-0901" />
</leadConfig>

Here is the Plug-in code that reads above values to be used in whatever the logic within the Plug-in. Interestingly, you will notice how we use a constructor class where configurations are being read within. Then we use GetUnsecureConfigValue() method to read each value in the XML.

using System;
using Microsoft.Xrm.Sdk;
using System.ServiceModel;
using System.Xml;

namespace TrialPlugin
{
    public class LeadPreCreate : IPlugin
    {
        private string UnsecureConfig { get; set; }

        public LeadPreCreate(string unsecureConfig, string secureConfig)
        {
            if (string.IsNullOrEmpty(unsecureConfig))
                throw new InvalidPluginExecutionException("Plugin Configuration missing.");
            UnsecureConfig = unsecureConfig;
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            try
            {
                IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
                if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
                {
                    Entity TargetEnt = (Entity)context.InputParameters["Target"];
                    if (TargetEnt.LogicalName != "lead")
                        return;

                    IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    IOrganizationService service = factory.CreateOrganizationService(context.UserId);

                    string regionCode = GetUnsecureConfigValue("RegionCode");
                    //string regionCode = Regex.Replace(GetUnsecureConfigValue("RegionCode"), @"\t|\n|\r", "").Replace(" ", String.Empty);
                    string integrationKey = GetUnsecureConfigValue("IntegrationKey");
                    //string regionCode = Regex.Replace(GetUnsecureConfigValue("IntegrationKey"), @"\t|\n|\r", "").Replace(" ", String.Empty);

                    throw new InvalidPluginExecutionException("RegionCode: " + regionCode + ", IntegrationKey: " + integrationKey);
                }
            }
            catch (FaultException<OrganizationServiceFault> e)
            {
                throw e;
            }
        }
        
        private string GetUnsecureConfigValue(string key)
        {
            const string XPATH = "leadConfig/setting[@name='{0}']";
            string configVal = string.Empty;
            try
            {
                var xmlDoc = new XmlDocument();
                xmlDoc.LoadXml(UnsecureConfig);
                var node = xmlDoc.SelectSingleNode(string.Format(XPATH, key));
                return node == null ? configVal : node.Attributes["value"].Value;
            }
            catch
            {
                return configVal;
            }
        }
    }
}

Caution
If you have lengthy values in XML, there is a chance that line-breaks and spaces being added without your intention. In such cases, you may use some Regular Expression functions to omit those. For above code, I have read the configuration data in two ways and commented out one. If explained issue likely to be hitting your situation, you may use the commented line instead of what is used.

Anyway, this method of passing configuration data is can be used to keep parameters differently for different environments. What is important to know is solution imports are overriding these vales.

Aug 29, 2019

Connect to WebApi using ClientId and Secret

We can now register our Dynamics 365 CE in Azure Active Directory, so that platform can be accessed through different client applications using OAuth. Idea behind is client applications can securely access WebApi using just Client Id and Secret.

Below is a code snippet with C# in .NET Framework 4.6.2 to achieve it.

Click here to see how to register D365 CE in Azure

Only two NuGet packages required. (Please note one of them are not in latest version)


using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;


namespace ConnectWebApiWithClientId
{
    class Program
    {
        static void Main(string[] args)
        {
            Task.WaitAll(Task.Run(async () => await Auth()));
        }

        public static async Task Auth()
        {
            string url = "https://SumeTest.crm.dynamics.com";
            string clientId = "5d83f9es-a577-4s01-ab9b-9513e39k970c";
            string secret = "MvrHJ2T2YK7NabYFRSOfrEqLMME/1OMW8n6sVBA7zxI=";
            string apiVersion = "9.1";

            try
            {
                var userCredential = new ClientCredential(clientId, secret);
                string webApiUrl = $"{url}/api/data/v{apiVersion}/";

                var authParameters = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(webApiUrl)).Result;

                var authContext = new AuthenticationContext(authParameters.Authority, false);
                var authResult = await authContext.AcquireTokenAsync(url, userCredential);
                var authHeader = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

                using (var client = new HttpClient())
                {
                    client.BaseAddress = new Uri(webApiUrl);
                    client.DefaultRequestHeaders.Authorization = authHeader;

                    // Use the WhoAmI function
                    var response = client.GetAsync("WhoAmI").Result;
                    if (response.IsSuccessStatusCode)
                    {
                        Console.WriteLine("Authenticated Successfully");
                        Console.WriteLine("Environement : {0}", url);
                        Console.WriteLine();
                    }
                    else
                    {
                        Console.WriteLine("The request failed with a status of '{0}'", response.ReasonPhrase);
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
            Console.WriteLine("Click Enter to Exit Application");
            Console.ReadLine();
        }
    }
}

Hope this helps.

Aug 20, 2019

Programmatical formatting of System Job Error string

When analyzing the errors in Dynamics 365 CE, we usually need to check System Jobs. In some cases, it may be required to create file (in most cases a Excel/ csv) with errors for further analysis. In such cases, it would be a troublesome exercise when it comes to handle the error message programmatically because they contain all kinds of error-prone characteristics such as spaces, many characters those become painful especially if you going to create importable file.

For example, consider below typical error message;

[Mxd.Crm.Workflows.Integration: Mxd.Crm.Workflows.Integration.CreateSalesData]
[Mxd.Crm.Workflows.Integration (1.0.0.0): Mxd.Crm.Workflows.Integration.CreateSalesData]

Correlation Id: a72ec1c9-ba30-422f-9b11-fcc7fd269e33, Initiating User: 0c089e94-0b42-e911-a878-000d3a6a0a90
Initialise Sales Manager 
Call to SalesManager to create Integration Request 
Retrieving Salesman (CONTACT) for "" [689825e6-1b90-e966-a882-000d3a6a065c]
Retrieving all Sales Codes for Salesman
Total number of Sales Enteries: 1
Retrieving ERP Data for Cales Code "East" [8180hh6c-2ac8-e711-a825-000d3ae0a7f8]
Data Area = "888"
WARNING: Region does not specify Regional Manager
Retrieving Salesman Profile "Jay Thompson" [d411a654-1c90-e911-a882-000d3a6a065c]
Invalid Plugin Execution exception detected: An item with the same sames code has already been added.



Error Message:

Unhandled exception: 
Exception type: Microsoft.Xrm.Sdk.InvalidPluginExecutionException
Message: An item with the same sames code has already been added.

Anyway, below is the simple method I wrote to format this, so that clean string is passed. This is so simple but time someone need to spend on this could be really costly. So thought of sharing.

public static string CleanErrorMsg(string valString)
{
var strWithSpaces = valString.Replace("\" ", " ").Replace(Environment.NewLine, " ").Replace(",", " ").Replace("\r\n", " ").Trim();
return Regex.Replace(strWithSpaces, @"\s+", " ");
}

This really removes below;
- troublesome characters
- Line breakers
- Extra spaces (more than one space together)

Nov 6, 2017

Use Service Calendar programmatically to check user availability

Service Calendar is a key component used in Field service for scheduling the work. Anyway, Calendar alone can be used for many different usages since its showing the working hours/ availability of a User. Here I am illustrating how to pro-grammatically access the calendar.

First of all, lets see how to set users availability in the calendar. In order to make it simple I am going to set if someone is available in given day or not. (not going into different time slots, which is also possible). Go to any User and go to Work Hours. Then go to New Weekly Schedule as below;


Please set the schedule as you wish in the resulting pane as below; Since I am interested in daily basis, I select 24 hours.


Once save you will see calendar been updated.


Now, this is the code snippet to check the availability of the User through calendar;

public static bool IsUserAvailable(IOrganizationService OrganizationService, Guid UserId)
{
    bool isUserAvailable = false;

    QueryScheduleRequest scheduleRequest = new QueryScheduleRequest
    {
        ResourceId = UserId,
        Start = DateTimeUtility.RetrieveLocalTimeFromUTCTime(OrganizationService, DateTime.Now),
        End = DateTimeUtility.RetrieveLocalTimeFromUTCTime(OrganizationService, DateTime.Now.AddSeconds(5)),
        TimeCodes = new TimeCode[] { TimeCode.Available }
    };
    QueryScheduleResponse scheduleResponse = (QueryScheduleResponse)OrganizationService.Execute(scheduleRequest);
    if (scheduleResponse.TimeInfos.Length > 0)
        isUserAvailable = true;

    return isUserAvailable;
}

You may notice that I am passing the local time in QueryScheduleRequest in above code since I am setting the local time zone in the Calendar also. I am also giving here the methods to be be used in time conversions;

internal static DateTime RetrieveLocalTimeFromUTCTime(IOrganizationService service, DateTime utcTime)
{
    return RetrieveLocalTimeFromUTCTime(utcTime, RetrieveCurrentUsersSettings(service), service);
}

internal static int? RetrieveCurrentUsersSettings(IOrganizationService service)
{
    var currentUserSettings = service.RetrieveMultiple(
        new QueryExpression("usersettings")
        {
            ColumnSet = new ColumnSet("timezonecode"),
            Criteria = new FilterExpression
            {
                Conditions =
                {
                            new ConditionExpression("systemuserid", ConditionOperator.EqualUserId)
                }
            }
        }).Entities[0].ToEntity<Entity>();
    return (int?)currentUserSettings.Attributes["timezonecode"];
}

internal static DateTime RetrieveLocalTimeFromUTCTime(DateTime utcTime, int? timeZoneCode, IOrganizationService service)
{
    if (!timeZoneCode.HasValue)
        return DateTime.Now;
    var request = new LocalTimeFromUtcTimeRequest
    {
        TimeZoneCode = timeZoneCode.Value,
        UtcTime = utcTime.ToUniversalTime()
    };
    var response = (LocalTimeFromUtcTimeResponse)service.Execute(request);
    return response.LocalTime;
}

Hope this helps!

Oct 19, 2017

Programmatically Share, Retrieve Shared Users and UnShare a Dynamics 365 record with a User OR Team

This is just to share some simple code snippets relates to sharing. This works well.

Sharing

using Microsoft.Crm.Sdk.Messages;

public static void ShareRecord(IOrganizationService OrganizationService, string entityName, Guid recordId, Guid UserId)
{
    EntityReference recordRef = new EntityReference(entityName, recordId);
    EntityReference User = new EntityReference(SystemUser.EntityLogicalName, UserId);

    //If its a team, Principal should be supplied with the team
    //EntityReference Team = new EntityReference(Team.EntityLogicalName, teamId);

    GrantAccessRequest grantAccessRequest = new GrantAccessRequest
    {
        PrincipalAccess = new PrincipalAccess
        {
            AccessMask = AccessRights.ReadAccess | AccessRights.WriteAccess | AccessRights.AppendToAccess | AccessRights.,
            Principal = User
            //Principal = Team
        },
        Target = recordRef
    };
    OrganizationService.Execute(grantAccessRequest);
}

Its great that VS intellisense would help you identify which Access Right you can set.


Retrieve user who has been shared with
public static void RetrieveSharedUsers(IOrganizationService OrganizationService, EntityReference entityRef)
{
    var accessRequest = new RetrieveSharedPrincipalsAndAccessRequest
    {
        Target = entityRef
    };
    var accessResponse = (RetrieveSharedPrincipalsAndAccessResponse)OrganizationService.Execute(accessRequest);
    foreach (var principalAccess in accessResponse.PrincipalAccesses)
    {
        // principalAccess.Principal.Id - User Id
    }
}

Revoke the Share (UnShare)
 public static void RevokeShareRecord(IOrganizationService OrganizationService, string TargetEntityName, Guid TargetId, Guid UserId)
 {
    EntityReference target = new EntityReference(TargetEntityName, TargetId);
    EntityReference User = new EntityReference(SystemUser.EntityLogicalName, UserId);

    RevokeAccessRequest revokeAccessRequest = new RevokeAccessRequest
    {
        Revokee = User,
        Target = target
    };
    OrganizationService.Execute(revokeAccessRequest);
}