LINQPad connections to Dynamics 365 online – Part 3 – Using myextensions to encapsulate your connection code

In the first part of this multi-part series of articles about creating connections in LINQPad to Dynamics 365 online, I went through the basic steps needed to create an OrganizationService connection in a LINQPad script.

The second article explained how to manage MFA protected accounts.

In this third article, I will explain how we can improve things by encapsulating all of this logic into a couple of useful methods which allow us to connect to Dynamics 365 by simply typing a single line of code.

Using ‘My Extensions’ to encapsulate this into a single ‘login’ command

Clearly, we don’t want to have to type all of the code from the previous articles into LINQPad every time we want to connect to Dynamics365. Using the MyExtensions class, we can very easily encapsulate all of the above logic into a single method, so that when you want to create a  connection to one of your Dynamics 365 instances, all you need to type is something like this:

IOrganizationService service = MyExtensions.ConnectToCrm(MyExtensions.Environments.DEV);

The MyExtensions class allows us to create reusable methods and other reusable components. To access the file, you can find and double click on the file from the ‘My Queries’ pane, or by using the shortcut Shift+Ctrl+Y:
My Extensions in the My Queries Pane
When you first open the MyExtensions file, you will see something like this:
My extensions before editing

Let’s start by defining an enum, listing the different environment we might want to connect to:

public enum Environments
	{
		DEV,
		TST,
		UAT,
		PRD
	}

Now you can add a ‘ConnectToCrm’ method in MyExtensions which takes one of these ‘environments’ as a parameter, and use a switch statement to build your connection string and connect to Dynamics 365:

public static IOrganizationService ConnectToCrm(Environments environment)
{
	// Your code here - it might look something like this...
	System.Diagnostics.Debug.Listeners.Clear();
	string username = Util.GetPassword("d365_username");
	string password = Util.GetPassword("d365_password");

	string instance = "";

	switch (environment)
	{
		case Environments.DEV:
			instance = "thenameofyourdevinstance";
			break;

		case Environments.TST:
			instance = "thenameofyourtestinstance";
			break;
        // switch for other environments...
	}

	string connectionString = "RequireNewInstance=True;AuthType=Office365;Url=https://"+ instance + ".crm11.dynamics.com/; Username=" + username + "; Password=" + password;
	
	ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

	OrganizationServiceProxy orgService = new CrmServiceClient(connectionString).OrganizationServiceProxy;

	Console.WriteLine("Connected to " + orgService.ServiceConfiguration.ServiceEndpoints.First().Value.Address.Uri);
	Entity user = orgService.Retrieve("systemuser", ((WhoAmIResponse)orgService.Execute(new WhoAmIRequest())).UserId, new ColumnSet("fullname", "domainname"));
	Console.WriteLine("Logged in as: " + user["fullname"] + " Domain Name: " + user["domainname"] + "\n");

	return orgService;
}

NB: You must compile your MyExtensions file by hitting f5 before the code becomes available in your other scripts.

Now all you need to do to connect to the environment of your choice in any LINQPad script is to select one of your preconfigured environments:
Choosing from a lost of environments

Connecting to multiple environments at the same time

One of the advantages in using this method of logging in, compared to a pre-configured LINQPad connector, is that it allows you to connect to more than one environment in a single script, to compare data or maybe to move data from one environment to another.

void Main()
{
    IOrganizationService svcDev = MyExtensions.ConnectToCrm(MyExtensions.Environments.D365DEV);    
    IOrganizationService svcTest = MyExtensions.ConnectToCrm(MyExtensions.Environments.D365TST);
    
    CountUsers(svcDev).Dump("# Users in Dev");
    CountUsers(svcTest).Dump("# Users in Test");	
}

private int CountUsers(IOrganizationService service)
{
    return ((EntityCollection)service.RetrieveMultiple(new QueryExpression("systemuser"))).Entities.Count;
}

which will give a result like this:
Comparing the number of users in different environments

Do you really want to log into production?
Another feature which you can add to your MyExtensions code is a warning when a user is about to log into production. This can be very useful in preventing someone from accidentally running a script against a production instance. We can easily do this by using LINQPad’s Util.ReadLine method to prompt the user to confirm if they want to connect to Production:

public static bool WarnIfConnectingToProduction(Environments environment)
{
	//Warn if user is connecting to a Production environment
	switch (environment)
	{
		case Environments.PRD:
		String carryOn = Util.ReadLine<String>("Connecting to a production environment - Do you want to continue? Y/N").ToUpper();

		if (carryOn != "Y" && carryOn != "N")
		{
			Console.WriteLine("You should have typed Y or N");
			Console.WriteLine("Process aborted");
			return false;
		}
		else if (carryOn == "N")
		{
			Console.WriteLine("Process aborted");
			return false;
		}
		break;
	}
        return true;
}

A prompt will appear at the bottom of the page, asking for confirmation when a Production instance is chosen:
Warning if connecting to Production instance

Adding Impersonation to our MyExtensions code
In this article, I explained how impersonating users can be very useful in diagnosing problems, or testing what happens when code as different users with different security roles. Adding an Impersonate method to your MyExtensions method can make this very straightforward.

Impersonating Another User

Sharing the love

Once you have created your methods in your Myextensions file, you can share this with the rest of the team, using the method outlined in this article. If you all set your  ‘Plugins and Extensions’ folder to the same folder in your Source Control system, you can all connect to Dynamics in exactly the same way without having to share any passwords.

Posted in Dynamics 365, LINQPad | Leave a comment

LINQPad connections to Dynamics 365 online – Part 2 – MFA

Handling MFA connections to Dynamics 365
In the first part of this multi-part series of articles about creating connections in LINQPad to Dynamics 365 online, I went through the basic steps needed to create an OrganizationService connection in your LINQPad script.

In this second article, I will explain how to proceed if your account is additionally protected by Multi-Factor Authentication (MFA). When using MFA to authenticate against Dynamics 365, you are prompted to authenticate with an additional token, such as a code sent via text message or an authenticator app. In order to avoid having to repeatedly authenticate this way you can use an Azure App Registration so that connections made from LINQPad are pre-authenticated. First of all you have to create an App Registration in Azure which will allow you to register your ‘App’ (in this case your connection from LINQPad with Azure Active Directory). Log into the Azure Portal and open the App Registrations blade. You will see a screen like this:App registration in Azure Portal

Give your app registration a name, and provide a ‘Redirect URI’. For our purposes, this does not need to be a real URI, but it must be in the format of a real URI.

Click Register and you’ll be taken to a page like this:

Client ID in App Registration Screen

You will use the Application (client) ID as part of your connection string.

Make the following amendments to your connection string in your LINQPad script:

  • Change AuthType from Office365 to OAuth;
  • Add the following additional parameters:
  • AppId: This should be the token you copied from the Azure Portal.
  • RedirectUri: This should be the Redirect URI you entered when setting up the App Registration.
  • LoginPrompt=Auto;
  • TokenCacheStorePath: The path to a location on your computer where a token will be stored after the first successful login, such as C:\temp\LinqPadOauth\cache.txt This will be used as the location in which the authentication token is stored so that you do not need to enter your credentials every time you log in.
  • Remove the Password parameter altogether.

Your connection string should now look something like this:
AuthType=OAuth;Url=https://yourInstance.crm11.dynamics.com/;AppId=123449g6-45e9-489b-8eab-h20d45ea60e9;RedirectUri=https://anyoldurl.doesntmatter.com;LoginPrompt=Auto;Username=your.username@yourO365domain.onmicrosoft.com;RequireNewInstance=True;TokenCacheStorePath=C:\temp\LinqPadOauth\cache.txt

Running our script again, we should now get prompted once to enter our password and MFA token, but after that our credentials will be cached and we will not be prompted to enter our password again.

However, you will see that our connection generates an awful lot of diagnostic output, which can make it difficult to see what’s going on….

Verbose output

Luckily, there is a way to suppress all of this diagnostic information. Add this line to your code, and you will get a nice clean output again:

System.Diagnostics.Debug.Listeners.Clear();

In the next article in the series, I’ll show you how you can encapsulate all of this code into a method so that you can easily create a connection in LINQPad by just typing a single line of code.

Posted in Dynamics 365, Dynamics CRM, LINQPad | Tagged , | Leave a comment

LINQPad connections to Dynamics 365 online – Part 1 – The Basics

LINQPad is an invaluable tool for .Net developers in general and for Dynamics developers being able to execute code using the Dynamics365 API using LINQPad can be a real timesaver. Whilst there is a LINQPad driver available for Dynamics here, it has not been updated for a while and I’ve never been able to connect to Dynamics 365 using this driver.
Even if you do manage to get the driver to work, there will be times when you want to create a connection to Dynamics 365 manually – for example if you want to be able to connect to multiple instances in order to be able to compare two environments, or to copy data from one instance to another.

In this multi-part series of articles, I will show you how to configure LINQPad so that creating connections to Dynamics365 can be done quickly and easily. Using the MyExtensions file, you’ll be able to create a shared method for every member of your team to be able to connect to any of your Dynamics environments by just typing a single line of code.

Creating a connection to D365 – the Basics
Let’s start with creating a basic connection to Dynamics 365, using a standard Microsoft Active Directory account.
Create a new LINQPad file of type C# Statement, and add references to System.Net.dll and Microsoft.CrmSdk.XrmTooling.CoreAssembly. (If you have the premium edition of LINQPad, you can download the latter using the integrated NuGet Manager, otherwise you’ll have to download it outside of LINQPad and browse to the file.)

Here’s the code you need to create an organisation service connection:

string username = "YourUsername@YourDomain.onmicrosoft.com";
string password = "YourPassword";

string connectionString = "RequireNewInstance=True;AuthType=Office365;Url=https://YourInstance.crm11.dynamics.com/; Username=" + username + "; Password=" + password;

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
CrmServiceClient connection = new CrmServiceClient(connectionString);

OrganizationServiceProxy orgService = connection.OrganizationServiceProxy;

There are a couple of things to notice here.
Firstly, note the use of TLS 1.2. Microsoft discontinued the use of TLS 1.0 or 1.1 with the Dynamics 365 platform in 2018, so all connections must be made using TLS 1.2.
Secondly, notice the use of the ‘RequireNewInstance‘ option. By default, this is set to False, which in many circumstances is sensible as it allows for the caching of the connection, which makes for faster execution. However, the disadvantage of this is that, if you make a connection to one instance of Dynamics 365, and then immediately to another instance (let’s say you are copying data from one instance to another in a script), you way well find that the second connection is to the same instance which you first connected to, which can be very confusing, if not downright dangerous!

So, now we can make a connection, but there are some further improvements which we might want to make.

Confirmation that we are logged in
Strangely enough, if you use invalid credentials in this code, you will not receive any error. The connection will fail silently, which is not very helpful.

We can, however, trap any errors by inspecting the ‘LastCrmError’ property of the connection:

if (!string.IsNullOrEmpty(connection.LastCrmError))
	{
		connection.LastCrmError.Dump("Last CRM Error");
		connection.LastCrmException.Dump("Last CRM Exception");
		throw new Exception(connection.LastCrmException.Message);
	}

We can quickly validate that we are logged in to the expected instance by displaying the details of the instance URI as follows:

Console.WriteLine("Connected to " + orgService .ServiceConfiguration.ServiceEndpoints.First().Value.Address.Uri);

and we can display the username of the user we are logged in as by performing a WhoAmI request:

Entity user = orgService.Retrieve("systemuser", ((WhoAmIResponse)orgService.Execute(new WhoAmIRequest())).UserId, new ColumnSet("fullname", "domainname"));
Console.WriteLine("Logged in as: " + user["fullname"] + " Domain Name: " + user["domainname"] + "\n");

Can you keep a secret? Using LINQPad’s Password Manager
The next improvement to be made is to start using LINQPad’s password manager so that we don’t keep our passwords in our scripts. Replace the variable holding the password (Line 2 in the first snippet) with this:

string password = Util.GetPassword("MyD365Password");

The first time that you run the script, you will be prompted to enter your password:

Every time you run the script after that, LINQPad will retrieve the password from its secure password store, so you don’t need to enter it every time.
This will also enable the sharing of scripts with other users without having to share login credentials – the passwords are not saved with the script, so each user will be prompted to enter their own credentials. You can save any information in the password manager, so this can also be useful to hold usernames so these are also not saved in scripts. Passwords can be managed (created/updated/deleted) directly in LINQPad, from the File -> Password Manager menu item.

In the next part in this series, I will show you how to manage connections for accounts which use Multi-Factor Authentication.

Posted in Dynamics 365, Dynamics CRM, LINQPad | Tagged , | Leave a comment

Using the Plugin Registration tool with MFA-enabled accounts in Dynamics 365

I was finding it impossible to connect to Dynamics 365 using the Plugin Registration Tool with an account which has Multi-Factor Authentication enabled.

With older versions of the Toolkit, I was just getting stuck in a loop where I’d successfully authenticate using MFA, be shown a list of instances, select one and then just get taken straight back to the initial login page.

With the latest version of the tool (9.1.0.1), I was getting a different problem – it wasn’t even showing me a list of instances, just giving me this error message after entering my username and password: Error : You don’t have permission to access any of the organizations in the Microsoft Common Data Service region that you specified. If you’re not sure which region your organization resides in, choose “Don’t know” for the CDS region and try again. Otherwise check with your CDS administrator.

Capturing the web traffic in Fiddler didn’t help much – I could see that the call was returning this message: “Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access” but this still didn’t help me getting the problem solved.

It wasn’t until I spoke to an engineer at Microsoft (Thanks, Trenton) that I was able to resolve the issue. When using MFA with the Plugin Registration Tool, it turns out that you must not enter your username and password in the login dialogue box!

Plugin Registration Tool Dialogue Box

If you leave these fields blank, when you click Login, you will get taken to the Microsoft Log-In screen as normal and this time your login will actually work!

If you think this counter-intuitive UI could be improved, you could up-vote this idea posted on the PowerApps forum.

NB If you want to get hold of the latest version of the Plugin Registration Tool, you will need to download it from the NuGet repository as described here.

 

 

 

Posted in Dynamics 365, Uncategorized | Tagged , | Leave a comment

Creating an OrganizationService connection to Dynamics 365 Online v9.0

I recently tried to use the sample code in the Dynamics 365 SDK (also available here) to create a connection in code to the Dynamics CRM v9 Organisation service. No matter what I did, I could not get the sample code to work. I was just getting a ‘System.NullReferenceException’ error, and the only other clue was that the connection object’s LastCrmError property had a value of “Unable to Login to Dynamics CRM Organization Web Proxy Client is null:

After a significant amount of head-scratching, a colleague reminded me that Microsoft have recently announced that connections to Dynamics 365 v9 will only work if using TLS 1.2 or higher.
Once we’d established this was the problem, fixing the issue was quite easy: before creating the connection to CRM, you need to switch to using TLS 1.2, which can be done like this:
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
using the ServicePointManager in System.Net.

Until Microsoft update their sample code to include this, I hope that this saves someone the problems I had in getting the connection to work.

With thanks to Ian Cummings for helping me to resolve this issue!

Posted in Uncategorized | Leave a comment

Programmatically removing attributes from user views in Dynamics CRM

If you have used an attribute in a saved view in Dynamics, and the attribute is removed from the entity, you will get the following error when you try and access the view:

Whilst it is straightforward to amend the view definition in Dynamics and remove the attribute from the view, we recently had a situation where many users had taken a system view containing the now-deleted attribute as the starting point for their own views, and thus several dozen users had views with the deleted attribute in it. Rather than wait for the support calls to come flooding in when users were presented with an error message which they did not understand, we decided to write a script which would go through each users views and remove the offending attribute.

The process is quite simple:

  1. Using impersonation, loop through each user in the system and query their user views for views containing the attribute.
  2. For each view containing the attribute in question, edit the ‘fetchxml’ and ‘layoutxml’ attributes, removing the references to the attribute
  3. Save the views

Here’s some sample code which would remove two attributes from all views belonging to a particular set of users. Amend the attribute, entities and user filter to tailor to your own requirements.

void Main()
{
    Console.WriteLine("Start");

    // Connect to CRM Organisation Service - you will need to change this to suit however you normally connect to CRM
    OrganizationServiceProxy service = MyExtensions.ConnectToCrm("dev2");

    // This is the entity which has had the attribute(s) removed
    string entityName = "bsp_trv_insuranceapplication";

    EntityMetadata em = GetEntityMetadata(entityName, service);

    // List of attributes to be removed from the view
    // You must list all attributes which no longer exist here, as you will not be able to save the views if there are ANY invalid
    // attributes in the view definition you are changing
    string[] attributesToBeDeleted = new string[] { "bsp_additionalinformation", "bsp_trv_addressln3" };

    // Get the users which might have views with the deleted attributes in - you may need to alter this query - eg restrict by Business Unit?
    EntityCollection users = GetUsers(service);

    foreach (var user in users.Entities)
    {
        RemoveAttributesFromUserViews(service, user, em.ObjectTypeCode, attributesToBeDeleted);
    }

    Console.WriteLine("Finished");
}

void RemoveAttributesFromUserViews(OrganizationServiceProxy service, Entity user, int? objectTypeCode, string[] attributesToBeDeleted)
{
    try
    {
        Console.WriteLine("Checking views for user " + user["domainname"]);

        // Impersonate the user
        service.CallerId = user.Id;

        // Filter for any views for the entity which contain the attributes to be deleted and which belong to the current user            
        string filter = @"<filter type='and'>
                <condition attribute='ownerid' operator='eq' value='" + user.Id + "' />";
        filter += @"<condition attribute='returnedtypecode' operator='eq' value='" + objectTypeCode + "' />";
        filter += @"<filter type='or' >";

        foreach (string attributeToBeDeleted in attributesToBeDeleted)
        {
            filter += @"<condition attribute='fetchxml' operator='like' value='%" + attributeToBeDeleted + "%' />";
        }
        filter += @"</filter>";
        filter += @"</filter>";

        // Get their queries
        string FetchXmlUserViews = @"<fetch mapping='logical'>
                            <entity name='userquery'>
                                <all-attributes/>    
                                {0}
                            </entity>
                            </fetch>";
                            
        EntityCollection userViews = service.RetrieveMultiple(new FetchExpression(string.Format(FetchXmlUserViews, filter)));

        Console.WriteLine("Found " + userViews.Entities.Count + " views");

        if (userViews.Entities.Count > 0)
        {
            foreach (Entity userQuery in userViews.Entities)
            {
                foreach (string attributeToBeDeleted in attributesToBeDeleted)
                {
                    // Remove node from fetchxml
                    string attributeToBeAmended = "fetchxml";
                    userQuery[attributeToBeAmended] = RemoveNodeFromAttribute(userQuery, attributeToBeDeleted, attributeToBeAmended);

                    // Remove node from layoutxml
                    attributeToBeAmended = "layoutxml";
                    userQuery[attributeToBeAmended] = RemoveNodeFromAttribute(userQuery, attributeToBeDeleted, attributeToBeAmended);
                }

                // Update the view
                try
                {
                    service.Update(userQuery);
                    Console.WriteLine("Attributes removed from view " + userQuery["name"]);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error for user " + user["fullname"] + " on view " + userQuery["name"]);
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("\n");
                }
            }
        }
    }
    catch (Exception ex)
    {
        if (!ex.Message.Contains("no roles are assigned to user"))
        {
            Console.WriteLine("Error for user " + user["fullname"]);
            Console.WriteLine(ex.Message);
        }
    }
}

public EntityCollection GetUsers(OrganizationServiceProxy service)
{
    EntityCollection results = service.RetrieveMultiple(new QueryExpression
    {
        EntityName = "systemuser",
        ColumnSet = new ColumnSet("fullname", "firstname", "lastname", "domainname"),
        Criteria = new Microsoft.Xrm.Sdk.Query.FilterExpression
        {
            FilterOperator = LogicalOperator.And,
            Conditions =
                {
        new Microsoft.Xrm.Sdk.Query.ConditionExpression
        {
            AttributeName = "lastname",
            Operator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal,
            Values = { "Walker" }
        },
        new Microsoft.Xrm.Sdk.Query.ConditionExpression
        {
            AttributeName = "isdisabled",
            Operator = Microsoft.Xrm.Sdk.Query.ConditionOperator.Equal,
            Values = { false }
        }

    },
        }
    });

    return results;

}


public String RemoveNodeFromAttribute(Entity userQuery, string attributeToBeDeleted, string nameOfAttributeToBeAmended)
{
    string xpath;
    string attributeToBeAmended = userQuery[nameOfAttributeToBeAmended].ToString();

    if (nameOfAttributeToBeAmended == "fetchxml")
    {
        xpath = @"//attribute[@name='" + attributeToBeDeleted + "']";
    }
    else
    {
        xpath = "//cell[@name='" + attributeToBeDeleted + "']";
    }

    XmlDocument doc = new XmlDocument();
    doc.LoadXml(attributeToBeAmended);

    XmlNode root = doc.DocumentElement;
    XmlNode node = root?.SelectSingleNode(xpath);

    if (node != null)
    {
        node.ParentNode?.RemoveChild(node);
    }

    return doc.InnerXml;
}

public EntityMetadata GetEntityMetadata(string entityName, IOrganizationService service)
{
    RetrieveEntityRequest retrieveRequest = new RetrieveEntityRequest
    {
        LogicalName = entityName
    };

    try
    {
        Console.WriteLine("Looking for entity called " + entityName);
        RetrieveEntityResponse retrieveEntityResponse =
            (RetrieveEntityResponse)service.Execute(retrieveRequest);

        return retrieveEntityResponse.EntityMetadata;
    }
    catch (Exception e)
    {
        Console.WriteLine("GetEntityMetadata found no data");
        Console.WriteLine(e.Message);
        return null;
    }
}

 

Posted in Dynamics CRM | Tagged | Leave a comment

Can I get single quotes with my FetchXML, please?

“I’d like my FetchXML with double quotes, please”, said no Dynamics developer, ever.

If you ever export FetchXML from the Advanced Find tool in Dynamics, you’re most likely doing this because you want to use that FetchXML in code somewhere. Isn’t it annoying, therefore, that FetchXML exported from the tool uses double-quotes to denote XML attribute values, when the first thing you will have to do every time is to change the double quotes to single quotes before you can use the FetchXML in your .Net code?

I don’t have a solution for that issue, per se, but many developers may not be aware that the FetchXML Builder tool in the XRM Toolbox has an option to render all FetchXML using single quotes.

This means not only that FetchXML rendered from the Fetch XML Builder (FXB) tool itself can be rendered in a more developer-friendly way, but also that any of the other plugins in the toolbox that use the FXB, such as the View Designer, can similarly be used to produce nicely formatted single-quoted XML.

You can even paste FetchXML exported from CRM into the builder, close and open it again and it will have replaced all double with single quotes…

Before formatting in FXB

After Formatting in FXB

Posted in Dynamics CRM, Uncategorized | Tagged | Leave a comment

Programmatically removing components from a MS Dynamics form or view

The ‘additive’ nature of CRM deployments means that it can be easier to add components to an instance of CRM than it is to remove them. With the additive model, even if you delete a component from your development environment and then export and deploy the solution which contained that component elsewhere, the deleted component will still exist in the target environment. As a result of this, there is a danger that CRM deployments can become littered with redundant components as the cost of the housekeeping required to remove these components from production environments can be seen as quite high.

As part of our Continuous Integration process, we have started to remove redundant artifacts from CRM. For example, attributes, entities and workflows which are no longer needed are removed to keep the solutions as clean as possible. Scripting the housekeeping tasks is actually quite straightforward, and once you have done this a couple of times you will see that it is easy to create a set of standard scripts to remove redundant components.

Removing entities and their relationships can all be scripted using standard calls to the metadata API such as DeleteRelationshipRequest and DeleteEntityRequest. However, these components cannot be deleted if they exist on a form. Therefore if you want to be able to automate the deletion process, you will need to script the removal of the items from the forms on which they appear.

Imagine the scenario where you have two entities in a one-to-many relationship, neither of which is required any more. Entity A is the ‘parent’ of entity B. The Entity A form contains a section with an associated view of Entity B and the Entity B form displays a lookup to Entity B.

To remove the entities manually, the process would be:

  1. Remove any references to either entity from either entity’s forms
  2. Delete the relationship between Entity A and Entity B
  3. Delete Entities A and B

The process has to be carried out in this order because:

  • You cannot delete a custom entity whilst it has a relationship to another custom entity
  • You cannot delete a relationship between entities if that entity is ‘in use’ on a form (either as a lookup for a 1:N relationship or an associated view for a N:1 relationship)

Due to the additive nature of Dynamics, as explained earlier, deleting the entities and then exporting the solution which contained them from our Dev environment and importing into another environment will not result in the entity being deleted from the target organisation. So we will need to write a script to remove the components from our target organisations. Deleting relationships and entities is a straightforward activity and we can easily script this using the DeleteRelationshipRequest and DeleteEntityRequest methods. However, we have to deal with the forms first and even if we do this in our development environment first, this will not help in other environments because the solution we export from Dev will not have our amended forms in them because we have deleted the entities from Dev.

Fortunately, scripting the removal of fields or sections from a form can be done quite easily. We can write a query which retrieves the form xml, identify the xml node which needs removing, update the form xml and publish the new version of the form.

The script will have three stages:

1 – Retrieve the Form XML for the entity in question

            // Get the formxml for the entity
            QueryExpression formQuery = new QueryExpression
            {
                EntityName = "systemform",
                ColumnSet = new ColumnSet(true),
                Criteria = new FilterExpression
                {
                    Conditions =
                    {
                        new ConditionExpression
                        {
                            AttributeName = "objecttypecode",
                            Operator = ConditionOperator.Equal,
                            Values = {entitytypeCodeOfTheEntity}
                        }
                    }
                }
            };

2 – Remove references to the entity from the form XML
For each form record returned in the above query, we can cast the formXML attribute as XML, and search for the particular node which references the item we want to remove. Once we have found the relevant node, we can then remove it from the formXml, and update the formXml on the entity and update it.

We will use an XPath expression to search the XML for a particular node. The XPath expression will depend on whether we are looking for an entity (a lookup field) or a section (containing an associated view).

                string xpath;                
                string searchString;
                if (!string.IsNullOrEmpty(sectionName))
                {
                    xpath = "//section[@name='" + sectionName + "']";
                    searchString = sectionName;                    
                }
                else if (!string.IsNullOrEmpty(attributeName))
                {
                    xpath = "//cell[control/@id='" + attributeName + "']";
                    searchString = attributeName;
                }

If the XPath expression returns a result, we can then remove the node and update the form:

                string formXml = form["formxml"].ToString();

                if (formXml.Contains(searchString))
                {
                    Console.WriteLine("Form contains " + searchString);

                    // Remove the section from the document
                    XmlDocument doc = new XmlDocument();
                    doc.LoadXml(formXml);

                    XmlNode root = doc.DocumentElement;
                    XmlNode node = root?.SelectSingleNode(xpath);

                    if (node == null)
                    {
                        xpath = "//tab[labels/label/@description='" + searchString + "']";
                        node = root?.SelectSingleNode(xpath);
                    }

                    if (node != null)
                    {
                        node.ParentNode?.RemoveChild(node);
                        Console.WriteLine("Node has been removed  ");
                    }
                    else
                    {
                        Console.WriteLine("Node not found");
                    }

                    form["formxml"] = doc.InnerXml;
                    service.Update(form);

3 – Publish the changes
Your changes to the form will not be permanent unless you publish them (which means you can practice the process in Dev until you are sure it is right).
You can publish changes to an entity using a method like this:

        public void PublishChangesToEntity(string entityName, IOrganizationService service)
        {
            Console.WriteLine("Publishing entity " + entityName);
            PublishXmlRequest request = new PublishXmlRequest
            {
                ParameterXml = @"<importexportxml>
                                       <entities>
                                          <entity>" + entityName + @"</entity>
                                       </entities>
                                       <nodes/>
                                       <securityroles/>
                                       <settings/>
                                       <workflows/>
                                    </importexportxml>"
            };

            try
            {
                var response = (PublishXmlResponse) service.Execute(request);
                Console.WriteLine("Changes to entity " + entityName + " published");
            }
            catch (Exception e)
            {
                // If entity not found, most likely has been deleted in previous run - not an error
                if (e.Message.Contains("was not found in the MetadataCache"))
                {
                    Console.WriteLine(e);
                }
                else
                {
                    Console.WriteLine(e);
                    throw;
                }
            }
        }

We will need to do the same if there are any views of Entity A which contain any attributes from Entity B – we will not be able to delete the relationship between the entities whilst these attributes are on the view. As before, we can query the fetchXml and the layoutXML of the view and update it. For example, we can use a fetchXml query like the one below to find any views based on a particular entity:

<fetch top='100' >
  <entity name='savedquery' >
    <all-attributes/>
    <filter type='and'>
       <condition attribute='returnedtypecode' operator='eq' value='{0}' />                  
    </filter>
  </entity>
</fetch></pre>
<pre>

and we can then modify the “layoutxml” and “fetchxml” attributes of the returned query definitions to remove any reference to the attributs which we want to remove.

With this technique it is easy to automate the process of removing sections or attributes from forms, or attributes from views, which will then enable you to programatically remove relationships between entities and the entities themselves.

Posted in CI, Dynamics CRM | Tagged | Leave a comment

Formatting the output of LINQPad’s DumpContainer

LINQPad’s Dump Containers can be used as a way of outputting some data to a static place within the output pane. A good example of this would be to display a timer or counter on the screen whilst a particular action is taking place.

A simple example of a Dump Container is as shown below where the content of the container is modified within a For loop.

Like LINQPad’s progress bar, Dump Containers can be really useful to indicate the progress of a running script. What makes them even more useful is that you can format the content of a Dump Container, which can be used to bring the user’s attention to something, such as an exception, or the completion of the script. You can easily amend the style of the container using css styling.

Here’s a trivial example of how the content and the style of the Dump Container can be changed on the fly:

You can download the second script from instant share here.

Posted in LINQPad | Tagged | Leave a comment

System.IO.FileLoadException and other errors in plugins deployed by CRM developer toolkit to Dynamics 2015

On creating a new plugins project for our Dynamics 2015 organisation with the latest version of the Dynamics 365 Developer Toolkit we were experiencing all sorts of errors, either on deployment or on execution.

On trying to register a plugin with IsolationMode=”None” we would experience this error:

System.IO.FileLoadException: Microsoft Dynamics CRM has experienced an error. Reference number for administrators or support: #156BBD18

Registering plugins in Sandbox isolation mode would work, but the plugins would fail on execution with this error:

SandboxAppDomainHelper.Execute: The plug-in type could not be found in the plug-in assembly

and

System.ArgumentNullException: Value cannot be null.

Trying to delete or re-register the step would give us this error:

Unable to load the plugin type

The issue turned out to be that, by default, the toolkit downloads the latest version (8.0.2.1) of the Microsoft.CrmSdk.CoreAssemblies package from NuGet, and this is a higher version than can be used by Dynamics CRM 2015. In order to fix this issue, we just downgraded the package using the NuGet Package Manager:

This fixes the problem and plugins will now work in either Sandbox or ‘None’ isolation mode.

Posted in Dynamics CRM, Uncategorized | Tagged | 1 Comment