Writing your first plugin in less than 1 hour

Dynamics 365 – Customer engagement

Before you start, you should know that I am not a developer. I can’t answer questions about development directly. This is intended for functional consultants that want to be able to make simple plugins without any developer knowledge.



I have an opportunity with a custom product table connected. I need the value of the product lines to synchronously sum up to the Opportunity when I hit save. Pretty much the same you would expect when you use the out of the box product entity.


Create a new trial for Dynamics 365 ( ). There are several ways to do this, but the link should work fine. Then download the Community Edition of Visual Studio (free in most cases, but be sure to read the license requirements). Under the installation just click next next etc. The first time you need to perform a choice is here:


Make sure you choose the .net desktop checkbox. Then continue to click next next and wait until you are done.
Next create a folder on c:/ I created one called D365Tools


Now open PowerShell and navigate to folder
Then open the URL:
Navigate to ths script part futher down the page, and copy the whole text. Use the Copy button in the top right corner


Paste the whole script to PowerShell (below is just a part of the code). Hit enter and let it finish.


When it is done, your folder should look like this:
We will be using the PluginRegistration and CoreTools later. Just in case, we will download the SDK framework 4.6.2 developer pack:


Just hit next next until done.

Visual Studio


We are now ready to open visual studio and start a new project.
Make sure you choose Class Library (.Net Framework), and then choose the .net 4.6.2 in the bottom left. I have chosen a path in my documents for storing the code. Just choose any location you want.
Now copy paste the following code:

using System;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk;
using CrmEarlyBound;
using System.Linq;
using Microsoft.Xrm.Sdk.Client;
namespace SumProduktLinjer
public class SumProduktlinjer : IPlugin
public void Execute(IServiceProvider serviceProvider)
IPluginExecutionContext context = (IPluginExecutionContext)
IOrganizationServiceFactory factory =
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
OrganizationServiceContext orgContext = new OrganizationServiceContext(service);
//This will give us the chance to log information to CRM so we can see what happens in the plugin
ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
//This will check to see that a plugin has fired on an entity. This is where we end up on create and update.
if (context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is Entity)
//Getting the actual entity. DO NOT CHANGE THE Image name here if you follow the guide.
Entity ProductLinePostImage = (Entity)context.PostEntityImages["Image"];
//Then i just check if the entity has a field called "ncg_salgsmulighetid" relationship to oppty you don't need this
if (ProductLinePostImage.Attributes.ContainsKey("ncg_salgsmulighetid"))
//Creating reference to the oppty
EntityReference ParentOpptyRefPost = (EntityReference)ProductLinePostImage.Attributes["ncg_salgsmulighetid"];
//Calling the search for product lines while expecting a decimal (total value) as return.
Decimal TotalProductLines = ProductLinesFetch(ParentOpptyRefPost, orgContext, tracingService);
//the same way you use alerts in javascript, you can use tracing service below.
//tracingService.Trace("Totalprislinjer fra Fetch: {0}", TotalProduktLinjer);
//Convert the desimal to a Money field
Money TotalproduktlinjerMoney = new Money(TotalProductLines);
//Create a new entity "object" in CRM. Use the GUID from the Post Image. And then use the total Money variable
//to update the estimated value of the opportunity you want to update.
Entity ParentOpptyUpdate = new Entity("opportunity");
ParentOpptyUpdate.Id = ParentOpptyRefPost.Id;
ParentOpptyUpdate.Attributes["estimatedvalue"] = TotalproduktlinjerMoney;
//When done defining what to update, we trigger the actual update to the parent
//This code will run when you delete a product line. Same check here to see if field exists
Entity PreProduktLinjer = (Entity)context.PreEntityImages["Image"];
if (PreProduktLinjer.Attributes.ContainsKey("ncg_salgsmulighetid"))
//Creating a reference to the parent "opportunity"
EntityReference ParentOpptyRefPre = (EntityReference)PreProduktLinjer.Attributes["ncg_salgsmulighetid"];
//Calling the search for product lines while expecting a decimal (total value) as return.
Decimal TotalProduktLinjer = ProductLinesFetch(ParentOpptyRefPre, orgContext, tracingService);
//Converting the result from our search to the Money we need.
Money TotalproduktlinjerMoney = new Money(TotalProduktLinjer);
//Create a new entity "object" in CRM. Use the GUID from the Post Image. And then use the total Money variable
//to update the estimated value of the opportunity you want to update.
Entity ParentOpptyUpdate = new Entity("opportunity");
ParentOpptyUpdate.Id = ParentOpptyRefPre.Id;
ParentOpptyUpdate.Attributes["estimatedvalue"] = TotalproduktlinjerMoney;
//When done defining what to update, we trigger the actual update to the parent
//This is the search function.
public Decimal ProductLinesFetch(EntityReference OpportunityRef, OrganizationServiceContext orgContext, ITracingService trace)
Decimal SumProductLines = 0;
//A lot like the advanced find. We start out by saying we are searching for the product line eneity.
//Then we say that filter is where product line opporutunity GUID is the one we want to update against.
IQueryable ProductLineQuery = orgContext.CreateQuery();
List listOfProductLines = ProductLineQuery.Where(p => p.ncg_SalgsmulighetId.Id == OpportunityRef.Id).ToList();
//then we loop through and add all of the lines to the SumProductLines before we return it.
foreach (var productLine in listOfProductLines)
SumProductLines += productLine.ncg_Pris.Value;
return SumProductLines;

Next, we add a reference to the SDK. Navigate to the folder we created and ran the PowerShell Script


Open browse
Find your folder for CoreTools and then choose the Microsoft.Xrm.Sdk.dll


Click OK, and you should now see lots of the RED errors disappear.
Next thing we need to do is add something called ProxyClasses. I am not sure how to explain this, but it is a list of all fields and available options on each entity we are working with.


Download XRM toolbox if you don’t already have it. Install the Early Bound Generator


Connect to the organization and open the Early Bound Generator. We only need 2 entities, so move all of the other ones over to the right.


Click create entities


Then open the option set.
Click “Create OptionSets”


Open the file location by choosing the option set relative path browse button.


Copy these files into a folder you call ProxyClasses in your project.
Add the folder ProxyClasses to the project


There should not be any more red errors in your code. If you have followed this correctly, it should all be ok.


Editing the code

Now we must edit the code. What you need to replace is the names of fields and entities if you are not using the same names as me. Look for the “ncg_salgsmulighetid”. This is the lookup on my custom product table linking to the opportunity.
Then we need to fix the query.


Here you have to change the “ncg_produktlinje” to the entity you created as a child to oppty, and the “p.ncg_salgsmulighetId.Id”. It might be case sensitive here, so try and use the Schema name in the config.


Getting confused? Don’t worry, it makes perfect sense once you know what to look for? Just remember that I chose to use a new entity called “ncg_produktlinje”, and you will probably name it something else. The rest is just changing out the lookup value to opportunity from the child entity.
Now that the code has been done, there is one last thing we need to do before installing to Dynamics 365


Open properties in the project
Choose Signing


Choose sign the assembly and new. Write something and choose a password. Click OK when you are done.
Now we can build the solution (right click on project or hit F6)


In the bottom of the screen you should see something like this:


Registering the plugin

Open the tools you downloaded earlier in the guide


Create a new connection and connect to your CRM system. In the list of organizations. Choose the one you have created the entities for.




Now locate the plugin you created as .dll file. Open your project in file explorer, and you will find a bin/Debug folder. Here you should find the DLL. In my case it was “SumProduktLinjer.dll”.


Now hit register plugin. When done, click close. You can now refresh the view to see the plugin.
The next thing we need to do is register steps and images for Create/Update/Delete. The step is a definition of when it should fire, and the image is a definition of what the product line looks like before or after the actual save to the database. If you don’t understand what I mean, just copy what I do.


Register the step, and then we create an Image


Make sure you choose Post Image, because on the create there is no Pre. The record doesn’t exist yet. If you wonder why I call it image here? It is because our code refers to this as image.


Now we have to do the same for Update and Delete.


IMPORTANT Delete command does not use post image. Only PRE image


The reason is that there is nothing there after the delete. Therefore, we have to see what the value was before the delete was done in the database.
The result should be something like this:


3 steps and 3 images.

The final result

Create new opportunity and hit save. Now add a new product from the new product subgrid


(this is quick create in Norwegian)


After you save, refresh the opportunity


In the upper right-hand corner (Estimated revenue) is now 100 nok? NB!! Remember that there is a setting for opportunity that is either system calculated, or user defined for Estimated Revenue. Make sure it is user defined.
If you get any other errors, you need to look at the attribute names in your code. Make sure they are 100% correct, and that they might be case sensitive in the query.
Congratulations. You have now created the first plugin.


Migrate SharePoint site content with Sharegate using powershell

Migrate SharePoint site content with Sharegate using powershell

Here’s an example of how to use PowerShell to migrate content from SharePoint On-premises to Office365 or SharePoint 2016. I use PowerShell to export the On-premises lists to a csv-file, add parameters to the copy-commandlet to adjust the migration type (e.g. only changed files since last migration), also Scheduling tasks to run off peak hours.
Read on 🙂

Step 1 – Get lists!

First things first, let’s get a csv file to work with. You can use the following example with PowerShell ISE:

#Export lists to CSV
Import-Module Sharegate
#Site to get the lists from, add username and password if it's different from the windows credentials
$username = "username"
$password = ConvertTo-SecureString "password" -AsPlainText -Force
$site = Connect-Site -Url "" UserName $username -Password $password
#Path to export the CSV to, will create the folder if it doesn’t exist
$csvExportPath = "C:SharegateReportscsv"
if(!(Test-Path -Path $csvExportPath -PathType Container))
New-Item -ItemType directory -Path $csvExportPath
#Get lists from the specified site and export it to CSV
$lists = Get-List -Site $site | Export-Csv -Path "$($csvExportPath)Example.csv" -Encoding UTF8 -Delimiter ","

Supply the username and password for the site you want to connect to. You can remove the password parameter if you want a password prompt every time you run the script.
It will create a folder if needed to export the csv-file to. Change the value to where you want the csv-file.
The last part will get all the lists in the specified site and export the list to a csv-file. You can limit the lists you get by adding the -Name parameter. This parameter is wildcard supported. An example: $lists = Get-List -Site $site -Name Example,A*
This will only get the list called Example and every list starting with “A”. For detailed information about the Get-List commandlet, visit

Step 2 – Copy Lists!

Now that we have a csv-file with the lists we want to copy, we can use it to copy content. Here’s an example:

#Copy lists from a csv-file
Import-Module Sharegate
#Specify the csv containing the lists you want to copy, example "C:example.csv"
$csv = "C:example.csv"
$lists = Import-Csv -Path $csv -Encoding UTF8 | select -ExpandProperty title
#The source of the copy, add username and password if it's different from the windows credentials
$sourceUrl = ""
$userNameSource = "username"
$passwordSource = ConvertTo-SecureString "password" -AsPlainText -Force
#Connecting to the source
$sourceSite = Connect-Site -Url $sourceUrl -UserName $userName -Password $password
#The destination of the copy, add username and password if it's different from the windows credentials
$destinationUrl = ""
$userNameDestination = "username"
$passwordDestination = ConvertTo-SecureString "password" -AsPlainText -Force
#Connecting to the destination
$destinationSite = Connect-Site -Url $destinationUrl -UserName $userName -Password $password
#Path used for reports. Will create the folder if it does not exist
$reportPath = "C:SharegateReports"
if(!(Test-Path -Path $reportPath -PathType Container))
New-Item -ItemType directory -Path $reportPath
#Incremental update setting, add -CopySettings $copySettings to the copy commandlet if you want to use incremental update
$copySettings = New-CopySettings -OnContentItemExists IncrementalUpdate
#Copy every list in the csv and export the reports
$counter = 0
foreach ($list in $lists)
Write-Progress -Activity 'Copying lists' -CurrentOperation $list -PercentComplete
(($counter / $lists.count) * 100)
$result = Copy-List -SourceSite $sourceSite -Name $list -DestinationSite $destinationSite
$result Export-Report $result -Path $reportPath -DefaultColumns

First you need to specify the csv-file you want to copy from. Then you need to specify the source and destination sites, along with the correct usernames and passwords.
Change the report path if you want to export the reports to a different path.
The foreach loop will go through every list in the csv-file and copy the lists to the destination site. You can add different parameters to the Copy-List commandlet to adjust the copy behavior. Incremental update for example you would add -CopySettings $copySettings
For detailed information about the Copy-List commandlet, visit
For the full Sharegate PowerShell documentation, visit

Scheduled task

Here’s a tip: Use Task Scheduler to schedule a migration. This can be helpful to plan a migration during off peak hours without having to start the script manually.


Happy Migration! 🙂


Updating single and multi value taxonomy fields using PnP-JS-Core

Point Taken is the leading SharePoint, Office 365 and Nintex consultancy in Norway.

All the devs in Point Taken are heavy users of the awesome repos provided on But even though our SharePoint dev lives have become easier we still encounter challenges and general SharePoint weirdness. This is one of those cases.

A little background: one of our clients wanted an application that would copy predefined tasks into project sites based on the project type. Some of these columns were taxonomy fields, both single and multi value. Updating term values in taxonomy fields using PnP-JS-Core (and REST in general) proved to be a bit of a struggle, and here are our discoveries.

Single value term fields can be updated using the code provided in this issue on GitHub

A single value taxonomy field is updated like this:

const listWeWantToUpdate = pnp.sp.web.lists.getByTitle('ListWeWantToUpdateTitle');

    .then((entityTypeFullName) => {

        const updateObject = {
            Title: 'Item title', // Only included as an example
            SomeSingleValueTaxonomyField: {
                __metadata: { type: 'SP.Taxonomy.TaxonomyFieldValue' },
                Label: 'LabelOfTerm', // field label - you can also use the Id returned in rest calls here
                TermGuid: '7dc9d5f8-16c2-48f2-88f7-90db39c7afb7', // field guid
                WssId: -1 // fake

        listWeWantToUpdate.items.add(updateObject, entityTypeFullName)
            .then((updateResult) => {
            .catch((updateError) => {

But what about fields with multiple values?

Based on the replies in the issue mentioned JSOM is the way to go, but where’s the fun in that? After trying the obvious variations of the code above it was time to turn to Google.

A bit of digging brought up blog posts by Jason Lee and Beau Cameron who had already cracked this nut in REST – the answer: use the InternalName of the hidden note field.

So first we need to get the corresponding note field and then we need to write a term string to it. Now we only had to figure out how to make this work in PnP-JS-Core.

A multi value taxonomyfield is updated like this:

const listWeWantToUpdateMulti = pnp.sp.web.lists.getByTitle('ListWeWantToUpdateTitle');

// Example of a term string.
// You can also use the Id returned in rest calls instead of the full label
const termString = '-1;#SomeTerm|02ee415b-99c4-448b-8727-7daa2a4a281;#-1;# SomeOtherTerm |0e2f40d9-09ac-406e-b102-630e8dadade6;';

// If the name of your taxonomy field is SomeMultiValueTaxonomyField, the name of your note field will be SomeMultiValueTaxonomyField_0
const multiTermNoteFieldName = 'SomeMultiValueTaxonomyField_0';

    .then((entityTypeFullName) => {
            .then((taxNoteField) => {
                const multiTermNoteField = taxNoteField.InternalName;
                const updateObject = {
                    Title: 'Item title', // Only included as an example
                updateObject[multiTermNoteField] = termString;

                listWeWantToUpdateMulti.items.add(updateObject, entityTypeFullName)
                    .then((updateResult) => {
                    .catch((updateError) => {

And there you go. Hope this information was useful to you

Update May 31st 2017:
A friendly Belgian reader pointed out that the code for multi value fields was missing an important step. The term string should be inserted into the corresponding note field and not the taxonomy field itself. The code has been updated to reflect this important detail.


Kjenner du deg igjen? Ta kontakt.

• Jeg er interessert i Azure, og ønsker å lære mer om det grunnleggende.

• Jeg har allerede Azure, og ønsker hjelp til å implementere løsningen.

• Jeg har allerede Azure, og ønsker hjelp med utfordringer i løsningen.

• Jeg trenger å lære mer om hvordan min bedrift kan bruke Azure på best mulig måte.

Knut Skogvold
90 09 50 88