Dynamics 365 & DevOps – Feedback Build

If you are developing software, never mind if it is open source on GitHub or inside a customer project, you have a good understanding of merging and branching. As long as you work alone, all is fine. As soon as you start working in a team’s and you still want to stay friends after the project, you might need to use more than task-tracking on Azure DevOps.

Without build support on Azure DevOps

Inside a project, usually multiple colleagues are working on several components of the system. In best case, they have their own feature branch to commit the changes. Of cause they will test, whatever has been modified regarding the current requirement. Maybe they add a total new component or they just add some lines in the existing code.

Their part is working and they commit the changes. Done.

Really done? I would say no. Of cause it was working on their machine and probably the changes are also fulfilling the requirement. But what, if they missed to add a component to the source control and it is not possible to build it on another machine or some of the unit tests are failing, because the developer just run his own tests? This is frustrating for the next colleague to make changes on that code, because he would need to fix it before he can start working.

The answer on this is the Feedback-build.

Requirements

Before you can start introducing a feedback build, I would suggest to make the following changes to your current process:

Branch-naming

Probably you are creating branches already. Start creating them in a specific sub-folder instead of having them on the same level as master, release or development. Git supports folder. Start creating your feature-branches always in a specific sub-folder. In my case I call that folder “features”. So all the feature branches will be below that folder.

You can create a new branch simply from the Azure DevOps UI or also from Visual Studio itself. Just navigate to Repos –> Branches and use the ‘…’ behind the branch you want to branch from and click on ‘+ New Branch’. Enter as Name “features/myfeature”.

1 Create Branch

In this case you will create a new folder called “features” automatically.

2 branch structure

Unit Tests

If possible I also strongly recommend to write some unit tests. Especially for Plugins and Workflow-Activities this makes fully sense. If you have written a test for the most important business requirements, you will be sure, you didn’t broke anything with your code change.

You can use any testing framework, that is supported by Azure DevOps, which literally means nearly anyone. I have chosen xUnit for myself. In some cases I also use FakeXrmEasy to execute the Plug in logic against an in-memory Dynamics.

Summary

With these pre-requisites in place, we are now ready to setup our feedback build. Of cause it also works without the unit tests, but having them in place is another level of quality you reach.

Setup your first Build

In your Azure DevOps environment and inside your project, navigate to “Pipelines –> Builds” and click “New Build Pipeline”. You can choose for the beginning the “Visual Designer”.

3 designer

On the next screen, “Team project”, “Repository” and “Default branch” should be preselected. Change them if you want need.

Final screen is about selecting a template. I always start with an “empty job”, but you can also use “.NET Desktop” and remove any task you don’t want.

Now you end up in the Editor. Main thing is, that you have to give it a name and assign an agent pool. There are some default agent pools you can choose from, but the performance is not the best. If you really want to run them fast, I suggest to host a custom agent.

Now you first build has a name, and will run on an agent of your choice. but nothing more. Save it the first time.

Create the needed tasks

If I need to setup a feedback build, I use the following 4 Tasks:

4 all tasks

NuGet Tool Installer

Here I want to have the latest NuGet version running, but also don’t want to loose caching to speed it up. So enter as version number 4.x. This will make sure, you always get the latest NuGet version used fitting that version.

5 task 1

NuGet

This task is used to restore your NuGet packages for your desired project. Select the command “restore” and also the path to your solution file.

6 task 2

Visual Studio Build

Now it is time to build. You need to select the Solution to build, the Visual Studio version you want to use, the platform and the configuration. You can move platform and configuration into variables, if not, use these values:

platform = any cpu
configuration = release

7 task 3

Visual Studio Tests

Finally I strongly recommend to have the test task included. Depending on your test framework, the configuration will be different. In my case I have used xUnit.

8 task 4

Triggers

The triggers will make sure, the build is running on each commit against a specific branch. Remember, we wanted to run it on all feature branches. Navigate to triggers and enable the continuous integration. In this scenario we build all commits into any feature-branch, but only, if something in the workflow-folder changed.

9 trigger 1

Options

Inside the options, you can configure the version numbering. For feedback builds I am using  a longer more readable version. This will be used in the e-mails later as well.

$(TeamProject)_$(BuildDefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)

You can also enable, that on each failed build the system should automatically create a bug and assign it to the person who triggered it.

91 trigger 2

Summary

After these steps you have configured your first build. I hope you and your team is enjoying the feedback build and is getting more productive. In the next article, I will show you how to create a development build and how you synchronize the configuration between the source control environment and your development environment.

Advertisements

Registering a WebHook – The basics

If you want to start with WebHooks, you always need to do the same steps. Check the official documentation for more information.

Setup a test environment

Before you register your WebHook in Dynamics 365, you should have some endpoint, that you can send your request too. If you just want to play around, like me, you can use an online service like https://webhook.site.

After you navigated to the site, it create an individual receiver just for you. All you need is to copy the unique URL and use it in the later steps. Keep in mind, the URL will not work anymore, when you closed the window. So might need to update the URL to be able to receive your request again.

image

When you send your first request, the site will directly show you the results. I usually adjust the following setting to have a better overview:

image

After we have now a nice receiver, we can start registering our first WebHook in Dynamics.

Register your first WebHook

Open the PluginRegistrationTool and connect to your Organization. Click on Register –> Register new WebHook.

image

You have to enter a speaking Name and copy the URL from your receiver. In our case we don’t have to care about the authentication, since we can use all. It will not be validated by the receiving test-site. However in your real-life environment you should take care about it.

image

Authentication Options

Any authentication option you choose seems to be secure. At least when you reopen the WebHook configuration with the PluginRegistrationTool, you can’t see the actual properties or the WebHookCode configured. If you need to change them, you have to reenter them again.

HTTP Header

HTTP Header allows you to send additional key-value-pairs as part of the header. You can specify multiple of them. You simply need to press the “+ Add Property” button and specify a key and a fitting value. The created properties are fix and cannot be modified during the final request.

If you configure something like this:

image

You will see the values in the header information:

image

WebHookKey

The WebHookKey is a fix HTTP query string. It is sending a query string “code” with the value, that you defined.

image

You will receive this:

image

HTTPQueryString

Finally this is the same as with the WebHookKey option, but you can define your own Properties and it can be more than one.

image

You receive this:

image

Register your first Step

To be able to test your first WebHook, just right-click the newly created WebHook in you PluginRegistrationTool and select “Register New Step”.

Go with a simple one and just enter “Update” as Message and “contact” as Primary Entity.
Finally make sure, it is running in PostOperation and Synchronous.

Whenever you update a contact from now on, the changes and the current context will be send as a WebHook request to your external receiver.

image

I just changed the email and received this:

image

{
  "BusinessUnitId": "08d80d40-b83e-e811-a94e-000d3a3899db",
  "CorrelationId": "81042d28-883c-4c96-af62-6fa310da7b1b",
  "Depth": 1,
  "InitiatingUserId": "7b3021e3-8c54-4d6a-9dc7-240a22c75596",
  "InputParameters": [
    {
      "key": "Target",
      "value": {
        "__type": "Entity:http://schemas.microsoft.com/xrm/2011/Contracts",
        "Attributes": [
          {
            "key": "emailaddress1",
            "value": "lars.mueller@crmpartners.com"
          },
          {
            "key": "contactid",
            "value": "e8a15bcf-e780-e811-a963-000d3a26c57a"
          },
          {
            "key": "modifiedon",
            "value": "/Date(1535821252000)/"
          },
          {
            "key": "modifiedby",
            "value": {
              "__type": "EntityReference:http://schemas.microsoft.com/xrm/2011/Contracts",
              "Id": "7b3021e3-8c54-4d6a-9dc7-240a22c75596",
              "KeyAttributes": [],
              "LogicalName": "systemuser",
              "Name": null,
              "RowVersion": null
            }
          },
          {
            "key": "modifiedonbehalfby",
            "value": null
          }
        ],
        "EntityState": null,
        "FormattedValues": [],
        "Id": "e8a15bcf-e780-e811-a963-000d3a26c57a",
        "KeyAttributes": [],
        "LogicalName": "contact",
        "RelatedEntities": [],
        "RowVersion": null
      }
    }
  ],
  "IsExecutingOffline": false,
  "IsInTransaction": true,
  "IsOfflinePlayback": false,
  "IsolationMode": 1,
  "MessageName": "Update",
  "Mode": 0,
  "OperationCreatedOn": "/Date(1535821252323)/",
  "OperationId": "3c47ab55-b4fe-4978-af19-15dc8c4f53e9",
  "OrganizationId": "c1b8622d-c398-4a86-8ef5-e7b7c872b4bc",
  "OrganizationName": "org01cfd5d6",
  "OutputParameters": [],
  "OwningExtension": {
    "Id": "177af12f-15ad-e811-a969-000d3a26cab0",
    "KeyAttributes": [],
    "LogicalName": "sdkmessageprocessingstep",
    "Name": "HookTester: Update of contact",
    "RowVersion": null
  },
  "ParentContext": {
    "BusinessUnitId": "08d80d40-b83e-e811-a94e-000d3a3899db",
    "CorrelationId": "81042d28-883c-4c96-af62-6fa310da7b1b",
    "Depth": 1,
    "InitiatingUserId": "7b3021e3-8c54-4d6a-9dc7-240a22c75596",
    "InputParameters": [
      {
        "key": "Target",
        "value": {
          "__type": "Entity:http://schemas.microsoft.com/xrm/2011/Contracts",
          "Attributes": [
            {
              "key": "emailaddress1",
              "value": "lars.mueller@crmpartners.com"
            },
            {
              "key": "contactid",
              "value": "e8a15bcf-e780-e811-a963-000d3a26c57a"
            }
          ],
          "EntityState": null,
          "FormattedValues": [],
          "Id": "e8a15bcf-e780-e811-a963-000d3a26c57a",
          "KeyAttributes": [],
          "LogicalName": "contact",
          "RelatedEntities": [],
          "RowVersion": null
        }
      },
      {
        "key": "SuppressDuplicateDetection",
        "value": false
      }
    ],
    "IsExecutingOffline": false,
    "IsInTransaction": true,
    "IsOfflinePlayback": false,
    "IsolationMode": 1,
    "MessageName": "Update",
    "Mode": 0,
    "OperationCreatedOn": "/Date(1535821252012)/",
    "OperationId": "3c47ab55-b4fe-4978-af19-15dc8c4f53e9",
    "OrganizationId": "c1b8622d-c398-4a86-8ef5-e7b7c872b4bc",
    "OrganizationName": "org01cfd5d6",
    "OutputParameters": [],
    "OwningExtension": {
      "Id": "c5cdbb1b-ea3e-db11-86a7-000a3a5473e8",
      "KeyAttributes": [],
      "LogicalName": "sdkmessageprocessingstep",
      "Name": "ObjectModel Implementation",
      "RowVersion": null
    },
    "ParentContext": null,
    "PostEntityImages": [],
    "PreEntityImages": [],
    "PrimaryEntityId": "e8a15bcf-e780-e811-a963-000d3a26c57a",
    "PrimaryEntityName": "contact",
    "RequestId": "3c47ab55-b4fe-4978-af19-15dc8c4f53e9",
    "SecondaryEntityName": "none",
    "SharedVariables": [
      {
        "key": "ChangedEntityTypes",
        "value": [
          {
            "__type": "KeyValuePairOfstringstring:#System.Collections.Generic",
            "key": "contact",
            "value": "Update"
          }
        ]
      }
    ],
    "Stage": 30,
    "UserId": "7b3021e3-8c54-4d6a-9dc7-240a22c75596"
  },
  "PostEntityImages": [],
  "PreEntityImages": [],
  "PrimaryEntityId": "e8a15bcf-e780-e811-a963-000d3a26c57a",
  "PrimaryEntityName": "contact",
  "RequestId": "3c47ab55-b4fe-4978-af19-15dc8c4f53e9",
  "SecondaryEntityName": "none",
  "SharedVariables": [],
  "Stage": 40,
  "UserId": "7b3021e3-8c54-4d6a-9dc7-240a22c75596"
}

Summary

This is a great an easy way to inform a remote service about the changes. However with this way you have no possibility to modify the content you send. Also the remote system only gets the changed data and need to be able to know how to handle this. You are also not able to enrich the context from related entities.

If something goes wrong, the user will see an error and cannot save.

In the next article we will walk through the simple plugin approach.

Dynamics WebHook Overview

Web Hooks can be used to handle server-events externally. This means it is possible to send data to an external REST-based webservice whenever a registered server-event happens. This can be after a record gets created, updated or deleted.

Communicating with the “outside” is possible in multiple ways. You can choose between the WebHook Model or the Azure Service Bus integration. In this article I will focus on the WebHook Model.

Decission finding WebHook vs Azure Service Bus integration

Based on the official documentation, theses are the things you should keep in mind:

  • Azure Service Bus works for high scale processing, and provides a full queueing mechanism if Dynamics 365 is pushing many events.
  • WebHooks can only scale to the point at which your hosted web service can handle the messages.
  • WebHooks enables synchronous and asynchronous steps. Azure Service Bus only allows for asynchronous steps.
  • WebHooks send POST requests with JSON payload and can be consumed by any programming language or web application hosted anywhere.
  • Both WebHooks and Azure Service Bus can be invoked from a plugin or custom workflow activity.

Basically there are several options to enable a WebHook request.

Options

A WebHook support 3 different ways of authentication.

You can register the WebHook request like a normal sdkmessageprocessingstep. The current context will be send as content. It it was registered synchronously, the user will see a business process error. With asynchronous registration, the user can see the status in the process history on the record, as a normal workflow response.

You can write a simple plugin and use the IServiceEndpointNotificationService to either also pass the context of the plugin or even create build together your own object to send.

Finally you can create a custom workflow-activity to execute a WebHook request, which would even allow you to reprocess the request on failure.

Keep in mind, that in the standard registration, only the current context will be submitted. This mean it only includes the changed fields and the general context (execution stage, user, organization…). When called through a plugin, you can even extend it, by registering a pre- or post-image.

Summary

WebHooks are pretty mighty and come with a lot of flexibility. Depending on your requirements, you can use them either without coding or extend the communication by some lines of code. It is even possible to include some retry functionality.

In the next articles, I will show you the different option, to implement a WebHook.

Links

Registering a WebHook – The basics

UI Testing with EasyRepro and ADFS

Last week I stubbled across a tweet from Wael Hamze. You might know him from his VSTS addon XRM CI Framework. This time he posted about the Microsoft UI Testing Framework for Dynamics 365 on his blog article.

I followed it and it was working really good, unless I struggled with the login. In my case the Dynamics 365 is still forwarding to an on-premise ADFS website to authenticate. And EasyRepro can only handle the O365 authentication out of the box.

Frustration

So I did some UI testing against my demo instance and figured out, that it works fine. Of cause it is not fast, as all the other tools I had a look so far. But I liked the easy approach of it.

Anyhow due to the login-limitations, I couldn’t create the tests against the system, where I wanted to automate the testing.

Optional Login Parameter was the solution

After a while, I figured out, that the Login-method does allow custom action to override the authentication part. And this is finally the solution.

instead of just calling the Login Method like this:

xrmBrowser.LoginPage.Login(_xrmUri, _username, _password);

I was calling it with a reference to a custom handler:

xrmBrowser.LoginPage.Login(_xrmUri, _username, _password, LoginViaAdfs);

Now I only had to overwrite the authentication part. I had to find the Input fields for username and password and click the submit button. Afterwards I had to disagree the “Stay Signed In” page and wait for Dynamics to come up.

Here is the resulting method I added:

public void LoginViaAdfs(LoginRedirectEventArgs args)
 {
     var d = args.Driver;

    // Username and password field by ID
     if (d.IsVisible(By.Id("ctl00_ContentPlaceHolder1_UsernameTextBox")))
     {
         d.FindElement(By.Id("ctl00_ContentPlaceHolder1_UsernameTextBox")).SendKeys(args.Username.ToUnsecureString());
     }

    if (d.IsVisible(By.Id("ctl00_ContentPlaceHolder1_PasswordTextBox")))
     {
         d.FindElement(By.Id("ctl00_ContentPlaceHolder1_PasswordTextBox")).SendKeys(args.Password.ToUnsecureString());
     }

    // Click the submit button
     if (d.IsVisible(By.Id("ctl00_ContentPlaceHolder1_SubmitButton")))
     {
         d.FindElement(By.Id("ctl00_ContentPlaceHolder1_SubmitButton")).Click(true);
     }

    // Wait for the "StaySignedIn"-Page and disagree
     d.WaitUntilVisible(By.XPath(Elements.Xpath[Reference.Login.StaySignedIn])
         , new TimeSpan(0, 0, 60),
         e => { e.WaitForPageToLoad(); },
         f => { throw new Exception("Login page failed."); });

    if (d.IsVisible(By.Id("idBtn_Back")))
     {
         d.FindElement(By.Id("idBtn_Back")).Click(true);
     }

    //Wait for CRM Page to load
     d.WaitUntilVisible(By.XPath(Elements.Xpath[Reference.Login.CrmMainPage])
         , new TimeSpan(0, 0, 60),
         e => { e.WaitForPageToLoad(); },
         f => { throw new Exception("Login page failed."); });
 }

Enjoy it and happy UI Testing even if ADFS is in use.

Automated Build of XrmToolBox Plugins with VSTS

After my 2 post about how to Build versioned XrmToolBox Plugins and how to Merge 3rd Party Assemblies into your Plugin, the next step will be to build everything automated.

My fellow and Microsoft MVP Jonas Rappen took the challenge and unveil his automated build using VSTS. Thank you very much for that.

General

These were my first steps into automated build and I was impressed, that I don’t have to host the code on VSTS, just to be able to get the automated build up and running. Following the instructions, I could setup my very first automated build, which of cause failed multiple times. *grrr*

I struggled with 2 thing. First of all, I was using ILMerge to include 3rd party assemblies. And second the assembly version and file version did not match the build number. Both are criteria’s, that need to be fulfilled for a trusted XrmToolBox plugin.

So far so good. How did I solved it?

ILMerge or ILRepack

I was a big fan of ILMerge, even if there are some drawbacks if you use it. But in my current situation to use it for automated builds, my build failed, when I was trying to call it with the exec MS build task.

I did some research and found ILRepack. This is a open source replacement for ILMerge which isn’t maintained since several year. The good thing is, that the exec build task was working fine here. Most probably with the same syntax it will work with ILMerge too.

I simply had to add the following NuGet Package:

image

This is, how I extended my project-file, to get the outcome merge repacked.

    <Target Name="ILRepack" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
         <MakeDir Directories="$(OutputPath)Merged" />
         <ItemGroup>
             <MergeAssemblies Include="$(OutputPath)\Microsoft.ApplicationInsights.dll" />
             <MergeAssemblies Include="$(OutputPath)\Newtonsoft.Json.dll" />
         </ItemGroup>
         <ItemGroup>
             <ILRepackPackage Include="$(NuGetPackageRoot)\ilrepack\*\tools\ilrepack.exe" />
         </ItemGroup>
         <Error Condition="!Exists(@(ILRepackPackage->'%(FullPath)'))" Text="You are trying to use the ILRepack
  package, but it is not installed or at the correct location" />
         <Exec Command="@(ILRepackPackage->'%(fullpath)') /out:$(OutputPath)Merged\$(AssemblyName).dll /target:library $(OutputPath)$(AssemblyName).dll @(MergeAssemblies->'%(FullPath)', ' ')" />
     </Target>

As you can see, I slightly modified the sample to and also added a line to create the “Merged” folder. On release build the assemblies will now be repacked.

This solved my first issue. However I would still not pass the criteria’s for releasing the Plugin into the store. Remember the build number?

Assembly Versions

In the blog from Jonas is pretty well written, how to use the build number inside the NuGet package. However it took me a while to handle the assembly version and the file version.

First of all I had to find the right task in the marketplace. I finally ended up with the Assembly Info task, which does a great job. I simply installed it and could use it in my build definition. It supports a lot of additional parameters.

I added it in between of the NuGet restore and the Build solution **\*.sln.

image

In the 3 fields for all version numbers I only need to put

$(Build.BuildNumber)

and it worked.

image

It is also possible to overwrite all other assembly attributes. You can even add the attributes, if they are not yet in your assembly, if you tick the “Insert Attributes”.

image

Finetuning

Finally I only had to finetune the Build number format in the Options. The format suggested by Jonas was inserting leading zeros on the file number format, that I didn’t wanted to have. So why I change the definition to:

$(Build.DefinitionName)-1.$(date:yyyy).$(Month).$(rev:r)

The result looks the same as from Jonas, but no leading zeros anymore.

Succeeded build:

image

This is the *.nuspec of the release drop.

image

and this the reflected *.dll using Jetbrains dotPeek.

image

Merge 3rd-party lib into your XrmToolbox Plugin

In some cases you might need to reference additional 3rd party libraries with your plugin. In this case keep in mind, that you are sharing the plugin-folder with all the other publisher. So it is more than possible, that you are referencing the same library as someone else, but in a different version number. As soon as there are more than one library with different version number, your plugin and probably also the other one will fail.

The easiest way around that, is to merge the 3rd party library simply into your plugin dll. In this example we are using ILMerge. I know it is already pretty old and has some drawbacks. However it works and I don’t want to start the discussion about nicer things.

To proceed just open your Plugin solution and follow the instructions.

Include ILMerge via NuGet

Search and reference for the package “ILMerge.Tools”.

image

Modify the Project-File manually

Navigate to the folder of your project file (*.csproj) and open it in an editor of your choice.

The modification will happen after the build. The script will only run on “Release” build and take the normal output file (your plugin dll) and merge it together with your 3rd party library (in this case the Microsoft.ApplicationInsight) and put it into a “Merged” subfolder of your “Release” folder.

Since the merged folder is not there, you also need to male sure, that the folder is created if needed.

First identify the Target of type “AfterBuild”. If you don’t have, you need to create one after the last “</PropertyGroup>” before the “</Project>” closing.

Add the following block:

<Target Name="AfterBuild" Condition="'$(Configuration)' == 'Release'">
   <ItemGroup>
     <MergeAssemblies Include="$(OutputPath)\{YourPluginName}.dll" />
     <MergeAssemblies Include="$(OutputPath)\Microsoft.ApplicationInsights.dll" />
   </ItemGroup>
   <PropertyGroup>
     <OutputAssembly>$(OutputPath)Merged\{YourPluginName}.dll</OutputAssembly>
   </PropertyGroup>
   <MakeDir Directories="$(OutputPath)Merged" />
   <Message Text="MERGING: @(MergeAssemblies->'%(Filename)') into $(OutputAssembly)" Importance="High" />
   <Exec Command="ilmerge /out:&quot;$(OutputAssembly)&quot; @(MergeAssemblies->'&quot;%(FullPath)&quot;', ' ')" />
 </Target>

Replace the placeholder {YourPluginName} with your real plugin name.

Save the project-file and reload it in your Visual Studio.

Build and Cleanup

If your build fails because ILMerge could not be found, make sure, you have the “Package Manager Console” open and it is initialized.

In the project-folder\bin\Release\Merged\ you will now find your merged dll which is slightly bigger.

Finally you have to adjust your nuspec file, since you want to use your merged assembly now instead of the plain one. So reference the one in the “Merged”-folder.

image

Summary

If you want to merge more 3rd party libraries, just add additional lines in the first ItemGroup like this:

<MergeAssemblies Include=”$(OutputPath)\SomeOtherAssemblyINeed.dll” />

That’s it.

Understand Virtual Entities–Part 1

Virtual entities exist for Dynamics 365 version 9.0 and that way also insider PowerApps. Basically it is more the other way around. When reading the doc.microsoft-article it gets clear, what they are used for.

You can show data from external data sources inside your Dynamics 365 without having the data stored. Please respect also the limitations.

Virtual entity diagram

Disassemble virtual entities

As we can see in the picture above, virtual entities are separated into multiple parts. You can name them:

1. External data provider

2. External data provider plugin

3. Virtual entity

Finally virtual entities are also “just” a normal entity with some additional attributes.

All starts with an External data provider. If you create one, it will generate a new entity in Dynamics 365. In this entity you can maintain all the attributes, that are needed to create a connection to your external data source. Of cause you can use the in-built OData provider. But again, review the limitations.

The External data provider plugin will be executed, when you consume the data provider with your virtual entity. The plugin can read the configuration information and is registered on Retrieve and Retrieve Multiple. But it is not a normal plugin. You need to take care about the query translation from a query expression coming from Dynamics 365 into the search against your external data service.

The last part is the virtual entity. It consumes one of your data providers. You have to maintain inside the virtual entity the external names. An external name is the name of the returned object of your data source. If you write your own adapter, you can do the mapping inside of your plugin. To be more generic, you can use the external name field on the virtual entity and it’s attributes.

Part 2 will cover, how to create your first own data provider using Plugin Registration Tool and Dynamics 365.