Using addPreSearch with an asynchronous call to create the filter

Hello everyone;

Recently while answering questions on the Microsoft Forums a question was raised about how to filter the query of an addPreSearch lookup event based on values in a relationship.

The scenario we’re looking at is we have a table called “Collection”.  This table has a M:1 relationship with director and it has a 1:M relationship with a table called Movies. The Movies table has a M:1 relationship with the director table. 

The requirement is to have the Director lookup prefilter and only show a list of directors from movies related to the collection. In the picture below when we click on the Directors lookup we would expect to see 6 directors selectable based on the associated movies.

Collection Table Form

How we go about this is using three specific methods:

The addPreSearch allows you to specify what event will fire when a user clicks on the lookup. In this function you to pass in a function and it must then add a custom filter.
– formContext.getControl(arg).addPreSearch(myFunction)

The addCustomFilter function is an event that executes when you click on the lookup field and executes the filter or fetchxml filter passed into it.
– formContext.getControl(arg).addCustomFilter(filter, entityLogicaName)

The Xrm.WebApi.retrieveMultipleRecords is a method that allows us to retrieve multiple records but this is an asynchronous call which causes the issue of building the proper filter in this scenario.

What we need to do to achieve the goal of filtering the directors lookup based on the related movies to the collection are the following steps:

  1. Execute a Xrm.WebApi.retrieveMultipleRecords allowing us to build the filter statement with the proper directors.
  2. Once the filter statement has been completed we need to call the addCustomerFilter function.
  3. We need to have our function that calls the addPreSearch when the lookup event is changed using the filter statement.

Let’s dive into the code. I’ll post my full web resource file and we can discuss it in more detail below. This web resource file can also be found on github at https://github.com/jasoncosman/Collections/blob/master/jcmvp_collection.js.

 

    OnLoad: function (executionContext) {
        JCMVP.Collection.Form.getCollectionsMovies(executionContext);
    },


    filterMovieDirectors: function () {
        //https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/addcustomfilter
        formContext.getControl("jcmvp_director").addCustomFilter(fetchDirectorFilter, "jcmvp_director");
    },


    getCollectionsMovies: function (executionContext) {
        formContext = executionContext.getFormContext();

        var collectionID = formContext.data.entity.getId().replace("{", "").replace("}", "");

        //https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/retrievemultiplerecords
        Xrm.WebApi.retrieveMultipleRecords("jcmvp_movie", "?$select=_jcmvp_director_value&$filter=(_jcmvp_collection_value eq " + collectionID + ")").then(
            function success(result) {
                debugger;

                if (result.entities.length > 0) {
                    fetchDirectorFilter = "<filter type='and'> <condition attribute = 'jcmvp_directorid' operator = 'in' uitype='jcmvp_director'>";
                }

                for (var i = 0; i < result.entities.length; i++) {
                    //_jcmvp_director_value
                    fetchDirectorFilter += "<value>" + result.entities[i]._jcmvp_director_value + "</value>";
                    console.log(result.entities[i]);
                }
                if (result.entities.length > 0) {
                    fetchDirectorFilter += "</condition ></filter >";
                    //https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/addcustomfilter
                    formContext.getControl("jcmvp_director").addPreSearch(JCMVP.Collection.Form.filterMovieDirectors);    
                }

            },
            function (error) {
                console.log(error.message);
                // handle error conditions
            }

            
        );

    }

We start with calling the getCollectionsMovies function on the form load event. This function will execute our query to get the related movies and director column filtering on the collection table id your currently on.
– Xrm.WebApi.retrieveMultipleRecords(“jcmvp_movie”, “?$select=_jcmvp_director_value&$filter=(_jcmvp_collection_value eq ” + collectionID + “)”).then(

This is an asynchronous call that when it completes it will work towards building our filter and storing it on our global variable “fetchDirectorFilter”. An example of the built filter statement looks like this:
“<filter type=’and’> <condition attribute = ‘jcmvp_directorid’ operator = ‘in’ uitype=’jcmvp_director’><value>889c9059-865d-eb11-a812-000d3a84f1a3</value><value>03fa2f78-865d-eb11-a812-000d3a84f1a3</value><value>889c9059-865d-eb11-a812-000d3a84f1a3</value><value>40d8e290-865d-eb11-a812-000d3a84f1a3</value><value>78d8e290-865d-eb11-a812-000d3a84f1a3</value><value>93034497-865d-eb11-a812-000d3a84f1a3</value><value>15920af3-6d5f-eb11-a812-000d3a84f1a3</value></condition ></filter >”.

After our filter statement has been created we’re going then go ahead and use the addPreSearch function. formContext.getControl(“jcmvp_director”).addPreSearch(JCMVP.Collection.Form.filterMovieDirectors);). This will allow for when the user clicks on the director lookup on the collection form it fires the filterMovieDirectors function.

When the user clicks on the director field it will execute the function filterMovieDiretors. This function will use our built filter and set it for the lookup. formContext.getControl(“jcmvp_director”).addCustomFilter(fetchDirectorFilter, “jcmvp_director”);

This code scenario is covering when the form loads or refreshes. What it doesn’t cover is updating the director list when a movie is added to the collection. I’ll work through this and post an additional blog post when I have that completed.

Clarifying how Business Rules work with Editable Grids

Let’s clarify how business rules work with editable grids once in for all. I keep seeing this discussed passionately and many people believe they don’t work together.

As a note the functionality we’re going to review today works the same for the UCI (Unified Client interface) or the classic editable grid. There currently is a bug with the unified interface version that I need to note here. I feel that this bug may be a cause for a lot of the debate. In writing this blog post it became very clear in the UCI when you switch from the read only grid to the editable grid on the home screen that business rules aren’t being applied. Once I see this issue resolved i’ll post an update to this.

Let’s create a simple business rule that says if the created on date contains data then lock the email field. I’ve tested the scope with all forms or entity and got the same result. In this scenario in order for this business rule to execute it must be able to validate the if condition; this means that the editable view must have the created on field in order to evaluate it. This seems to be something that alludes people frequently.

Checking this business rule against this simple view in the editable grid we see that the email is still editable.  As the view doesn’t contain the created on date field.

Let’s create a simpler business rule that checks to see if the email contains data and if it does then lock the email field. I’ve tested the scope with all forms or entity and got the same result. This covers a common scenario that wouldn’t allow the user to change a fields value once entered in the system on the form or editable grid.

Checking this business rule against this simple view in the editable grid we see that the email locked because it contains data. If we clicked on an account record that didn’t have an email value it would be editable until a value was saved in it.

I hope this brings some clarity to how business rules interact with editable grids.

The more you know!

Firing Business Rules when making JavaScript changes!

With the investment being made in Dynamics 365 around business rules I typically try and leverage them when possible. I’ve found at times it creates some challenges along the way. One specific challenge  a college and I’ve ran into was when making JavaScript modifications to fields the related business rules were’t executing.

Making the business rules fire took a bit of research but it paid off as we’re able to continue to use Business Rules with JavaScript. A co-worker of mine Mike Wright found the answer and documentation. Microsoft has the documentation for the fireOnChange event you need to use on the Attribute here.

When the fireOnChange event is called it will call your related javascript event for that attribute as well execute any business rules associated to it. This is a great piece of knowledge to know when working with business rules and javascript.
Xrm.Page.getAttribute(arg).fireOnChange()

The more you know!

Understanding the Dynamics 365 List Records connector retrieve limit

Hello;

Let’s talk about the retrieve limit of the Dynamics 365 connector that is used in Power Apps, Logic Apps, and Flow. While working with a large data set we noticed that the maximum amount of records per request is 512.

While looking at the settings we found the pagination setting and the ability to set an upper limit of query. Both of these made me research this a bit more and I found that tese settings were implemented back in August of 2017 with the original post found at: https://flow.microsoft.com/en-us/blog/four-connector-action-settings/.  It’s worth noting that the settings options have changed since then but this is where the pagination functionality was introduced.

Looking at the List Records (Previews) connectors settings today; it looks like this:
Dynamics List Records

The pagination is pretty straight forward it allows the connector to continue to make requests to get your full data set. Currently I have an outstanding question if each page counts as a individual execution; if it does this means that there is a cost associated with each page. Once I have an answer I’ll make sure to update this.

The Limit option allows us to specific our ceiling regardless of how many pages it goes through. I really think that if your going to be using a LogicApp or Flow to retrieve and update Dynamics 365 data that you have a good idea of how much traffic each action is expected to have. With this in mind I think its a best practice to understand and set a limit option on any List Records connectors if your going to enable pagination.

Hope this sparks some good conversation as these options were implemented in a simple way to use; which I really appreciate.

Understanding the Expand Query in the Dynamics 365 List Records connector – Part 2

Hello;

Part one of this post (Understanding the Expand Query in the Dynamics 365 List Records connector) showed how to utilize the expand query paramete. This post will covering actually using these values to update an account with the data. Let’s dive right into part two of using the expand query in the Dynamics 365 connector that is used in Power Apps, Logic Apps, and Flow.

We left off with our related primarycontactid data returned from our account request.
Dynamics 365 - List Records

Since we have our data the next thing is to take action on these returned records. We’re going to add an Apply to each  as the next step. We’ll add the value from dynamic content. This will be each record returned from the Account Query. We will want to choose an action. In this case we’re going to use the Data Operations – Parse JSON. Let’s go ahead and select this action.
Apply to Each

The Parse JSON action allows us to specify which returned field to parse on. This is an important step to get correct. Using the Dynamic Content you can search and select the Primary Contact item. The next step is critical; hovering over it to ensure it has the right field name(see expand query field for the proper name); in our case it’s “primarycontactid”. This is critical step as its very easy to get the wrong field here.
Parse JSON

Using the Parse JSON it’s required to enter the schema; luckily it allows us to paste data and it’ll generate this for us. In this case I’ve simply used the data returned for my account from the previous post.

Parse JSON
The generated schema comes out like the following.

{
    "type": "object",
    "properties": {
        "@@odata.type": {
            "type": "string"
        },
        "fullname": {
            "type": "string"
        },
        "firstname": {
            "type": "string"
        },
        "lastname": {
            "type": "string"
        }
    }
}

Now that the schema is completed; we’re now able to use this transformed data dynamically. We’re going to update the account name to include the contact full name. Adding a Dynamics 365 – Update record action is the first step. When clicking into any field you can now see the dynamic data of both the Parse JSON result and List Records(Previous) aka the account data.
Dynamics 365 - Update Record

The flow is now completed. Let’s execute this and double check within CRM to see if it updated properly. We can see the results of the update step.
Successful Execution

After double checking CRM to see our record updated I think we can call this a wrap.
Account

We’ve went through parsing the expanded query results and turning them into Dynamic Content to be used. There are a ton of other things that can be done with the different actions and conditions; but this is a basic example of how to use the Expand Query field in the connectors.

I hope you’ve found this helpful.!

Understanding the Expand Query in the Dynamics 365 List Records connector

Hello;

Recently I’ve been diving deeply into the Dynamics 365 connector that is used in Power Apps, Logic Apps, and Flow. I’ve found the documentation lacking when trying to do more advanced features. Today were discussing the Expand Query component of this connector.

First lets start with a new flow and add the Dynamics 365 – List Records(preview) connector.
Dynamics 365 - List Records

Once we have the connector added we’re going to retrieve all Accounts. We select an organization and then select which entity we’re going to use. This will get us all accounts in the organization.
Dynamics 365 - List Records

The Expand Query option is where we’re going to focus on. MSDN documents this field here.  The documentation states:

Expand Query
string
Related entries to include with requested entries (default = none)
Key:
$expand

This doesn’t really give us a practical example to work from. When researching online the majority of searches come back with using the relationship name of a relationship. This is actually incorrect. Let’s take the Primary Contact relationship on the Account entity as an example.
Dynamics 365 - List Records

For the Expand Query field it actually takes the lookup value of this relationship. In this case it is looking for the “primarycontactid”.

Let’s go back to our query and add this in.
Dynamics 365 - List Records

As you can see we’re able to specify the lookup field and then select exactly which columns to be retrieved from the Contact entity. The output of this query returns the related record in a json object.

      "primarycontactid": {
        "@odata.type""#Microsoft.Azure.Connectors.Crm.SubItem",
        "fullname""Jason Cosman",
        "firstname""Jason",
        "lastname""Cosman"
      }

Getting the related record is only half the battle as it isn’t able to be used as dynamic content yet. You have to parse it out and then use it. Stay tuned for the next post where we update the account with contact information using the retrieved data.

Don’t forget to upgrade assemblies for custom code in Dynamics 365 v9+ upgrade!

Upgrading to the latest platform is never something to underestimate with any product. With Dynamics 365 we’ve experienced such rapid platform changes over the past 8 years we should be all getting used to some of the pieces of an upgrade.

With the Dynamics 365 V9+ update we have to remember to upgrade our custom code assemblies. This should be fairly straight forward if your using NuGet packages in your projects. Microsoft has invested into lots of different packages like https://www.nuget.org/packages/Microsoft.CrmSdk.CoreAssemblies/ .

I use Visual Studio for my custom code components like plugins and custom workflow activities.  After opening the project I’ve selected manage NuGet packages and I get a screen like this:

This update process also helps resolve any dependencies and highlights any errors if they occur. A great example of this is I mentioned before I’m using FakeXrmEasy as my unit test framework. This will need to be updated as well; in this example FakeXrmEasy has dependencies displayed on the bottom for the CrmSdk.CoreAssemblies less then version 9.0.0.

Since we’re upgrading into Dynamics 365 V9+ we need to make sure our unit test framework is using the right assemblies. In this case FakeXrmEasy provides a separate NuGet package for Dynamics 365 V9+; we’d simply remove the FakeXrmEasy.365 NuGet package and then install the new one.

Of course after doing this and deploying your assemblies into Dynamics 365 you have to test like crazy.

The more you know!

My experience writing unit tests with FakeXRMEasy

Hello;

I thoroughly enjoy writing unit tests with FakeXRMEasy. It’s a framework that allows you to fake the Dynamics 365 services into the proper contexts and then use the messages against it. It can be found at https://dynamicsvalue.com/.

The positives I’ve found from using this framework are:

  • I’m able to debug the custom code without ever having to leave Visual Studio.
    • No more spending time getting plugins to debug properly through visual studio
  • I’m able to replicate a wide variety of scenarios through data in my unit tests without having to step through everyone within the Dynamics 365 platform itself.
  • Works with custom workflow activities, plugins, portals, javascript.

Let’s walk through a piece of code. The scenario is we are building a sports application in Dynamics 365; when a game ends a plugin triggers. This plugin updates the winning team with the win as well a point total; then adds a loss to the losing team. Let’s examine the plugin first.

        public void Execute(IServiceProvider serviceProvider)
        {
            
            
            if(serviceProvider ==null)
            {
                throw new ArgumentNullException("serviceProvider", "serviceProvider cannot be a null refrence");
            }

            IPluginExecutionContext context = (IPluginExecutionContext)(serviceProvider.GetService(typeof(IPluginExecutionContext)));

            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            //Lets get the game information and update each teams record
            Entity target = (Entity)context.InputParameters["Target"];

            try
            {
                Entity aGame = null;

                tracingService.Trace("Let's get the game entity with all of it's columns. The ID is:" + target.Id.ToString());
                if(context.MessageName== "Update")
                {
                    aGame = (Entity)service.Retrieve(target.LogicalName, target.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));                
                }
                else
                {
                    throw new Exception("Plugin:EndGame - This plugin should only be executed against an update message not a create or delete.");
                }

                //Let's validate that it returned something
                if(aGame != null)
                {
                    EntityReference homeTeam = null;
                    if(aGame.Attributes.Contains("jmvp_hometeam"))
                    {
                        homeTeam = (EntityReference)aGame.Attributes["jmvp_hometeam"];
                    }

                    EntityReference awayTeam = null;
                    if (aGame.Attributes.Contains("jmvp_awayteam"))
                    {
                        awayTeam = (EntityReference)aGame.Attributes["jmvp_awayteam"];
                    }

                    OptionSetValue gameOutcome = null;
                    if (aGame.Attributes.Contains("jmvp_outcome"))
                    {
                        gameOutcome = (OptionSetValue)aGame.Attributes["jmvp_outcome"];
                        //Home - 492,470,000
                        //Away - 492,470,001
                        //Tie  - 492,470,002
                    }

                    Entity homeTeamEntity = getEntitywithFields(homeTeam, true, service);

                    Entity awayTeamEntity = getEntitywithFields(awayTeam, true, service);

                    switch(gameOutcome.Value)
                    {
                        //Home
                        case 492470000:
                            homeTeamEntity.Attributes["jmvp_winrecord"] = (int)homeTeamEntity.Attributes["jmvp_winrecord"] + 1;
                            homeTeamEntity.Attributes["jmvp_totalpoints"] = (int)homeTeamEntity.Attributes["jmvp_totalpoints"] + 2;
                            awayTeamEntity.Attributes["jmvp_lossrecord"] = (int)awayTeamEntity.Attributes["jmvp_lossrecord"] + 1;
                            service.Update(homeTeamEntity);
                            service.Update(awayTeamEntity);
                            break;
                        //Away
                        case 492470001:
                            awayTeamEntity.Attributes["jmvp_winrecord"] = (int)awayTeamEntity.Attributes["jmvp_winrecord"] + 1;
                            awayTeamEntity.Attributes["jmvp_totalpoints"] = (int)awayTeamEntity.Attributes["jmvp_totalpoints"] + 2;
                            homeTeamEntity.Attributes["jmvp_lossrecord"] = (int)homeTeamEntity.Attributes["jmvp_lossrecord"] + 1;

                            service.Update(awayTeamEntity);
                            service.Update(homeTeamEntity);
                            break;
                        //Tie
                        case 492470002:
                            break;   



                    }


                }



            }
            catch(Exception ex)
            {
                throw (ex);
            }

        }

        private Entity getEntitywithFields(EntityReference entityToRetrieve, Boolean columnSet, IOrganizationService service)
        {

            return service.Retrieve(entityToRetrieve.LogicalName, entityToRetrieve.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(columnSet));


        }
    }

As you can see it simple does a check on the jmvp_outcome field to determine who won and updates appropriately. Now using FakeXRMEasy let’s walk through two separate unit tests. The first one is where the home team wins the game.

[TestMethod()]
        public void HomeTeamWins()
        {
            

            var fakedContext = new XrmFakedContext();

            var team1 = new Entity("jmvp_sportsteam");
            team1.Id = Guid.NewGuid();
            team1["jmvp_name"] = "Canadiens";
            team1["jmvp_winrecord"] = 10;
            team1["jmvp_lossrecord"] = 8;
            team1["jmvp_totalpoints"] = 20;

            var team2 = new Entity("jmvp_sportsteam");
            team2.Id = Guid.NewGuid();
            team2["jmvp_name"] = "Maple Leafs";
            team2["jmvp_winrecord"] = 12;
            team2["jmvp_lossrecord"] = 6;
            team2["jmvp_totalpoints"] = 24;

            var game1 = new Entity("jvmp_game");
            game1.Id = Guid.NewGuid();
            game1["jvmp_name"] = "November 5th - Canadiens vs Maple Leafs";
            game1["jmvp_hometeam"] = new EntityReference(team1.LogicalName, team1.Id);
            game1["jmvp_awayteam"] = new EntityReference(team2.LogicalName, team2.Id);
            game1["jmvp_outcome"] = new OptionSetValue(492470000);

            fakedContext.Initialize(new List<Entity>()
            {
                team1, team2, game1, game2
            });


            

            ParameterCollection inputParameters = new ParameterCollection();
            inputParameters.Add("Target", game1);

            var plugCtx = fakedContext.GetDefaultPluginContext();
            plugCtx.MessageName = "Update";
            plugCtx.InputParameters = inputParameters;
            plugCtx.Depth = 1;

            var FakedPlugin = fakedContext.ExecutePluginWith<EndGame>(plugCtx);

            IOrganizationService service = fakedContext.GetOrganizationService();

            Entity updatedHomeTeam = service.Retrieve(team1.LogicalName, team1.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

            Entity updatedAwayTeam = service.Retrieve(team2.LogicalName, team2.Id, new Microsoft.Xrm.Sdk.Query.ColumnSet(true));

            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_winrecord"] + 1), (int)updatedHomeTeam["jmvp_winrecord"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_totalpoints"] + 2), (int)updatedHomeTeam["jmvp_totalpoints"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team2["jmvp_lossrecord"] + 1), (int)updatedAwayTeam["jmvp_lossrecord"]);


        }

Let’s dive a bit deeper into this unit test. First we create the entities and data set. Then we load it into the faked context “XrmFakedContext”.

            var fakedContext = new XrmFakedContext();

            var team1 = new Entity("jmvp_sportsteam");
            team1.Id = Guid.NewGuid();
            team1["jmvp_name"] = "Canadiens";
            team1["jmvp_winrecord"] = 10;
            team1["jmvp_lossrecord"] = 8;
            team1["jmvp_totalpoints"] = 20;

            var team2 = new Entity("jmvp_sportsteam");
            team2.Id = Guid.NewGuid();
            team2["jmvp_name"] = "Maple Leafs";
            team2["jmvp_winrecord"] = 12;
            team2["jmvp_lossrecord"] = 6;
            team2["jmvp_totalpoints"] = 24;

            var game1 = new Entity("jvmp_game");
            game1.Id = Guid.NewGuid();
            game1["jvmp_name"] = "November 5th - Canadiens vs Maple Leafs";
            game1["jmvp_hometeam"] = new EntityReference(team1.LogicalName, team1.Id);
            game1["jmvp_awayteam"] = new EntityReference(team2.LogicalName, team2.Id);
            game1["jmvp_outcome"] = new OptionSetValue(492470000);

            fakedContext.Initialize(new List<Entity>()
            {
                team1, team2, game1, game2
            });

Once we’ve  initialized our fakedContext we’re ready to setup the input parameters and define some information around this plugin. When setting up the plugin information we’re able to select the message name which allows us to handle all of messages; which means we can unit test against any of the events. We set the entity that’s trigger the plugin and how much data we want to pass into it. Then i’m setting the depth of the plugin which can be important. The last thing that needs to be done is to actually execute the plugin itself.

            ParameterCollection inputParameters = new ParameterCollection();
            inputParameters.Add("Target", game1);

            var plugCtx = fakedContext.GetDefaultPluginContext();
            plugCtx.MessageName = "Update";
            plugCtx.InputParameters = inputParameters;
            plugCtx.Depth = 1;

            var FakedPlugin = fakedContext.ExecutePluginWith<EndGame>(plugCtx);

The last thing that I do is to validate my expected outcomes. In this case I want to ensure team 1’s properly got the additional points and win record. As well team 2’s loss total has increased by one.

            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_winrecord"] + 1), (int)updatedHomeTeam["jmvp_winrecord"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team1["jmvp_totalpoints"] + 2), (int)updatedHomeTeam["jmvp_totalpoints"]);
            Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(((int)team2["jmvp_lossrecord"] + 1), (int)updatedAwayTeam["jmvp_lossrecord"]);

Although this is a very simple example of a unit test; I think it gives a good example of whats within the capability of mocking up the fakedContext.

Hope this helps!

What is a system entity message?

Hello;

With the release of V9 we’ve seen new items in the solution structure and package. One such change is the Messages under each entity in the solution.

 

Messages give the ability to change the content displayed when the user sees these out of the box messages. It includes user interface text and error messages. It gives you the default display string and allows you to enter a custom display string and then a comment to explain the change. Editing is very straight forward and looks like this:

It’s worth noting that entity messages aren’t available for custom entities and that your only able to edit existing messages.

Managed Solutions V9 thoughts

The great debate between unmanaged and managed solutions rages on. This discussion started when solutions was first introduced in Dynamics CRM 2011. Many people go the route of unmanaged being burned by managed; each person with their own battle scars.

Personally I’ve been burned by unmanaged which makes on the managed side of the fence. It hasn’t been easy along the journey of these solutions but with the changes introduced in Dynamics CRM 2016 around patching and being able to actually remove technical debt or depreciated fields from a managed solution made the viability much higher in my opinion.

It’s worth considering Microsoft’s view point of managed solutions in every environment past development organizations. It can be easily viewed in the new v9 solution changes happening at the platform level. As the CRM product family has been expanding over the past few years we’ve seen the sales & service & marketing split into separately SKUs and available individually now. This has been done through managed solutions and Microsoft’s ability to layer them properly.

As an example of this I simply created a new solution and added the out of the box Account entity and exported it as unmanaged. When looking at the solution file it shows every dependency for the account entity and what solutions it comes from. A few sample are:

     <MissingDependency>
        <Required key="26" type="1" schemaName="territory" displayName="Territory" solution="msdynce_AppCommon (9.0.4.0022)" />
        <Dependent key="27" type="10" schemaName="msdyn_territory_account_ServiceTerritory" displayName="msdyn_territory_account_ServiceTerritory" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="28" type="1" schemaName="bookableresource" displayName="Bookable Resource" solution="msdynce_Scheduling (9.0.0.0)" />
        <Dependent key="29" type="10" schemaName="msdyn_bookableresource_account_PreferredResource" displayName="msdyn_bookableresource_account_PreferredResource" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="30" type="1" schemaName="msdyn_taxcode" displayName="Tax Code" solution="FieldService (7.4.0.74)" />
        <Dependent key="31" type="10" schemaName="msdyn_msdyn_taxcode_account_SalesTaxCode" displayName="msdyn_msdyn_taxcode_account_SalesTaxCode" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>
      <MissingDependency>
        <Required key="32" type="2" schemaName="name" displayName="Territory Name" parentSchemaName="territory" parentDisplayName="Territory" solution="msdynce_AppCommon (9.0.4.0022)" />
        <Dependent key="27" type="10" schemaName="msdyn_territory_account_ServiceTerritory" displayName="msdyn_territory_account_ServiceTerritory" parentSchemaName="account" parentDisplayName="Account" />
      </MissingDependency>

This gives us a bit of an inside view on solutions being used by Microsoft like “msdynce_Scheduling (9.0.0.0)” and “msdynce_AppCommon (9.0.4.0022)”. It’s really interesting how we’re able to see how they are splitting up the platform to enable different product offerings. It also shows the complexities in doing this with all of the out of the box entities. Needless to say V9 seems to be the first time we’re able to look around a bit more and understand where the platform team is coming from.

Hope you found this interesting because I sure do.