Examples

In this section we will add relevant examples for different business scenarios. We are aware that some of the examples are a bit teoretical, and there are other ways to solve the problems described. But the purpose of the example is to show how to solve a problem with code. The fact that there are other - maeby even better ways of solving - still allow the example to do a conceptual walk through of problems and solution.

Hello world

All framworks must have a hello world, so the first example on this page is named accordingly

The problem

Before we can write a plugin, we must have a problem to solve. So here it comes. The "requirement specification"

The customer wish to have a special log on change of ownership on accounts. (we know audit can solve this problem,... please ignore that fact.). We are asked to write a timestamp, plus the fullname of the owner into the Description field of the account (to avoid adding new fields). Any other type of changes to the Description field should be prevented, to ensure this log cannot be overridden.

The situation from an interface point of view:

namespace Kipon.PluginExample.Entities
{
    using Kipon.Xrm.Attributes;
    public partial class Account : Account.IOwnershipLogging
    {
        bool IOwnershipLogging.DescriptionChanged => this.TargetAttributes.ContainsKey(nameof(Description).ToLower());

        public interface IOwnershipLogging : IAccountMergedimage
        {
            [TargetFilter]
            Microsoft.Xrm.Sdk.EntityReference OwnerId { get; set; }

            [TargetFilter]
            string Description { get; set; }

            bool DescriptionChanged { get; }
        }
    }
}

The above interface, added as an "account-inner-class" definition describes the situation we wish to listen for. The interface enherits from [IAccountMergedimage]. That is a decorator interface generated by the Kipon Solid framework, and it is stating that anything extending this expects a merged image, and all properties marked with the [TargetFilter] attribute should be added to the target filter on plugin registration. The example is demonstrating a couple of cool things on the framework.
  • [IAccountMergedimage] declerative interface that state we are talking about the account entity, and we expect a merged image. (meaning all values not set by the client will be populated from the preimage)
  • [TargetFilter] allow you to pick the attributes we wish to add to the target filter for the plugin. The plugin registre with this method as parameter will only be called if a least one of the parameters marked is part of target payload.
  • [TargetAttributes] you can access TargetAttributes direcly in the partial Account instance. This allow you to build simple interface that indicates if something is part of target payload or not.
  • [Description get set] If you add setter to properties, you can set the values through this interface, and in validate- and pre-state, the value will drain into the target, and become part of the current running update for the entity.

So the basic statement here - from a business point of view is: We are interested in account situations where ownerid is assigned, and if description is assigned we are interested as well. We wish to be able to set the description, and it is import for us to know if the client has explicitly assigned the description. (we wish to prevent such thing.)


The business service interface
Now lets define a business service that can perform the logging - based on above situation interface:
namespace Kipon.PluginExample.ServiceAPI
{
    public interface IAccountOwnershipService
    {
        void LogOwnership(Entities.Account.IOwnershipLogging mergedimage);
    }
}


Implementing the business interface:

using Kipon.PluginExample.Entities;
using Kipon.Xrm.ServiceAPI;
using Microsoft.Xrm.Sdk;

namespace Kipon.PluginExample.Services
{
    public class AccountOwnershipService : ServiceAPI.IAccountOwnershipService
    {
        private readonly INamingService nameService;

        public AccountOwnershipService(Kipon.Xrm.ServiceAPI.INamingService nameService)
        {
            this.nameService = nameService;
        }

        public void LogOwnership(Account.IOwnershipLogging mergedimage)
        {
            if (mergedimage.DescriptionChanged)
            {
                throw new InvalidPluginExecutionException("Description should not be changed by any client code, it is a system maintained field");
            }

            if (mergedimage.Description == null)
            {
                mergedimage.Description = $"{ System.DateTime.Now.ToString("yyyy-MM-dd HH:mm") }:{this.nameService.NameOf(mergedimage.OwnerId) }";
            }
            else
            {
                mergedimage.Description = $"{ System.DateTime.Now.ToString("yyyy-MM-dd HH:mm") }:{ this.nameService.NameOf(mergedimage.OwnerId) }\r\n{ mergedimage.Description }";
            }
        }
    }
}

The implementation

The constructor of the implementation is taking a Kipon.Xrm.ServiceAPI.INamingService. The cool part about that service is that it can lookup the name attribute of any entity, so in this sitatuion it can be used to lookup the name of user/team assigned as ownerid. This is relevant because in most sitautions, the name parameter of an EntityReference is unassigned when draining though the create/update process

If the client has assigned the description explicitly we throws a InvalidPluginExecutionException. This is a std Microsoft SDK exception that will drain all the way to the UI as a popup business process error, and the create/update will be prevented

Finally we assign the description with datetime, and name from the ownership. Because this code is executed after our validation, any change by the framework it self of the description will be accepted.

Finally we need to write the plugin that wire together the business situation (event interface), and the business service:

namespace Kipon.PluginExample.Plugins.Account
{
    public class AccountOwnershipPlugin : Kipon.Xrm.BasePlugin
    {
        public void OnPreCreate(Entities.Account.IOwnershipLogging target, ServiceAPI.IAccountOwnershipService service)
        {
            service.LogOwnership(target);
        }

        public void OnPreUpdate(Entities.Account.IOwnershipLogging mergedimage, ServiceAPI.IAccountOwnershipService service)
        {
            service.LogOwnership(mergedimage);
        }
    }
}

First of all we extend Kipon.Xrm.BasePlugin.

Then we add two public methods, OnPreCreate and OnPreUpdate, taking the situation interface as parameter and the business service as parameter. The first parameter will tell the framework that this plugin should registre on Account Create Pre stage, and Account Update pre stage, and all properties in the interface that maps to a strongly type generated entity attribute should be included in the preimage of the update event, and all properties marked with [TargetFilter] should be included in the target filter of the update event.

Be aware that the OnPreCreate will be called regardless if any of the fields in the interfae is part of the payload, while the OnPreUpdate will only be called if at least one of the fields marked with [TargetFilter] is part of target payload (attribute was changed by the client).

Unit test

Ofcause we should also create a unit test.

Lets start with test for the AccountOwnershipService.

Remember, this is just a plain .NET service, with no dependencies to the Dynamics 365 plugin infrastructure, and as such, it can be tested with plain mock implementations.


using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Xrm.Sdk;
using System;

namespace Kipon.PluginExample.UT.Services
{
    [TestClass]
    public class AccountOwnershipServiceTest
    {
        #region mocks
        internal class OwnershipChanged : 
            Kipon.PluginExample.Entities.Account.IOwnershipLogging
        {
            public EntityReference OwnerId { get; set; }
            public string Description { get; set; }

            public bool DescriptionChanged { get; set; }

            public Guid Id => Guid.NewGuid();

            public string LogicalName => 
                Kipon.PluginExample.Entities.Account.EntityLogicalName;
        }

        internal class NameService : 
            Kipon.Xrm.ServiceAPI.INamingService
        {
            public string NameOf(EntityReference refid)
            {
                return "Kjeld Poulsen";
            }

            public string PrimaryAttributeId(string entitylogicalname)
            {
                throw new NotImplementedException();
            }

            public string PrimaryAttributeName(string entitylogicalname)
            {
                throw new NotImplementedException();
            }
        }
        #endregion

        #region tests

        [TestMethod]
        public void LogOwnershipPreventChangeOfDescriptionTest()
        {
            var service = new Kipon.PluginExample.Services.AccountOwnershipService(new NameService());
            var os = new OwnershipChanged { DescriptionChanged = true };

            Assert.ThrowsException<Microsoft.Xrm.Sdk.InvalidPluginExecutionException>(() => service.LogOwnership(os));
        }

        [TestMethod]
        public void LogOwnershipFirstOwnershipTest()
        {
            var service = new Kipon.PluginExample.Services.AccountOwnershipService(new NameService());
            var os = new OwnershipChanged { DescriptionChanged = false, OwnerId = new EntityReference(Entities.SystemUser.EntityLogicalName, Guid.NewGuid()) };
            var time = System.DateTime.Now;
            service.LogOwnership(os);

            Assert.AreEqual($"{ time.ToString("yyyy-MM-dd HH:mm") }:Kjeld Poulsen", os.Description);
        }

        [TestMethod]
        public void LogOwnershipAddOwnershipTest()
        {
            var currentLog = "2021-03-01 12:37:Peter Jensen";
            var service = new Kipon.PluginExample.Services.AccountOwnershipService(new NameService());
            var os = new OwnershipChanged { DescriptionChanged = false, OwnerId = new EntityReference(Entities.SystemUser.EntityLogicalName, Guid.NewGuid()), Description = currentLog };
            var time = System.DateTime.Now;
            service.LogOwnership(os);

            Assert.AreEqual($"{ time.ToString("yyyy-MM-dd HH:mm") }:Kjeld Poulsen\r\n{ currentLog }", os.Description);
        }
        #endregion
    }
}

First we create a simple mock interface for the event captured. In the real plugin, The Kipon.Solid framework ensure that we have an implementation of that by letting the strongly typed entity from the platform implement the interface representing the event captured. In unit test it is perfectly possible to create a simple mock that can be initiated with hardcoded values and parsed to the service interface method.

Secondly we create a simple mock for the Kipon.Xrm.ServiceAPI.INameService. The Kipon.Solid.Plugin framework does delivery an implementation of that service, but that implementation require a Dynamics 365 CE datasource from where it can map entityreferences to names, so to make things simple we create a mock that returns a hardcoede predictable name.

Finally we use the mocks to create som tests:

  • LogOwnershipPreventChangeOfDescriptionTest: Test that exception will be thrown if the client set the description explicitly
  • LogOwnershipFirstOwnershipTest: Test that description will be initiated correctly first time ownership is tested
  • LogOwnershipAddOwnershipTest: Test that description will be accumulated correctly when ownership is updated where ownership already have a value
Finally we create a unit test for the plugin.

Testing a plugin require a Dynamics 365 CE context. Luckely the Kipon.Solid.Plugin framework also have a "Fake" library that mocks the Dynamics 365 CE standard plugin service, but it also knows the way the Kipon.Solid.plugin framework wires the datamodel and the event with these services. Just add the nuget package Kipon.Solid.Plugin.Fake to your unit test library. Thats it. That makes it quite simple to create unit test for the plugin's as well. These test is using and in memory representation of the Dynamics 365 CE entity data.


using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace Kipon.PluginExample.UT.Plugins.Account
{
    [TestClass]
    public class AccountOwnershipPluginTest
    {
        [TestMethod]
        public void PreventSetDescriptionOnAccountCreateTest()
        {
            using (var ctx = Kipon.Xrm.Fake.Repository.PluginExecutionFakeContext.ForType<Kipon.PluginExample.Plugins.Account.AccountOwnershipPlugin>())
            {
                var account = new Kipon.PluginExample.Entities.Account { Description = "We set description, witch is not allowed" };
                Assert.ThrowsException<Microsoft.Xrm.Sdk.InvalidPluginExecutionException>(() => ctx.Create( account ));
            }
        }

        [TestMethod]
        public void PreventSetDescriptionOnAccountUpdateTest()
        {
            var accountid = Guid.NewGuid();
            using (var ctx = Kipon.Xrm.Fake.Repository.PluginExecutionFakeContext.ForType<Kipon.PluginExample.Plugins.Account.AccountOwnershipPlugin>())
            {
                ctx.AddEntity(new Kipon.PluginExample.Entities.Account { AccountId = accountid });

                var account = new Kipon.PluginExample.Entities.Account { AccountId = accountid,  Description = "We set description, witch is not allowed" };
                Assert.ThrowsException<Microsoft.Xrm.Sdk.InvalidPluginExecutionException>(() => ctx.Update(account));
            }
        }


        [TestMethod]
        public void SetDescriptionToOwnerOnCreate()
        {
            var systemuserid = Guid.NewGuid();

            var ownersfullname = "Kjeld Poulsen";
            var expect = $"{System.DateTime.Now.ToString("yyyy-MM-dd HH:mm")}:{ownersfullname}";

            using (var ctx = Kipon.Xrm.Fake.Repository.PluginExecutionFakeContext.ForType<Kipon.PluginExample.Plugins.Account.AccountOwnershipPlugin>())
            {
                ctx.AddEntity(new Kipon.PluginExample.Entities.SystemUser { SystemUserId = systemuserid, ["fullname"] = ownersfullname });

                var account = new Kipon.PluginExample.Entities.Account { OwnerId = new Microsoft.Xrm.Sdk.EntityReference(Kipon.PluginExample.Entities.SystemUser.EntityLogicalName, systemuserid) };

                ctx.OnPre = delegate
                {
                    Assert.AreEqual(expect, account.Description);
                };
                ctx.Create(account);
            }
        }

        [TestMethod]
        public void SetDescriptionToOwnerOnUpdate()
        {
            var accountid = Guid.NewGuid();
            var systemuserid = Guid.NewGuid();

            var ownersfullname = "Kjeld Poulsen";
            var currentDescription = "CURRENT VALUE";
            var expect = $"{System.DateTime.Now.ToString("yyyy-MM-dd HH:mm")}:{ownersfullname}\r\n{ currentDescription }";


            using (var ctx = Kipon.Xrm.Fake.Repository.PluginExecutionFakeContext.ForType<Kipon.PluginExample.Plugins.Account.AccountOwnershipPlugin>())
            {
                ctx.AddEntity(new Kipon.PluginExample.Entities.SystemUser { SystemUserId = systemuserid, ["fullname"] = ownersfullname });
                ctx.AddEntity(new Kipon.PluginExample.Entities.Account { AccountId = accountid, Description = currentDescription });

                var account = new Kipon.PluginExample.Entities.Account 
                { 
                    AccountId = accountid,
                    OwnerId = new Microsoft.Xrm.Sdk.EntityReference(Kipon.PluginExample.Entities.SystemUser.EntityLogicalName, systemuserid) 
                };

                ctx.OnPre = delegate
                {
                    Assert.AreEqual(expect, account.Description);
                };
                ctx.Update(account);
            }
        }
    }
}

All tests

The using statement is setting up a Dynamics 365 CE plugin pipeline context for a specific plugin. This basically represent the "Dynamics 365 CE" platform mock. By creating this context you now have a placeholder where you can initialize the datamodel you need to test, create the hooks for the events you expect to trigger, and finally trigger the event with hardcoded entity data

Specific tests
  • PreventSetDescriptionOnAccountCreateTest: Test that the plugin will prevent a create of an account if Description has been set
  • PreventSetDescriptionOnAccountUpdateTest: Test that hte plugin will prevent an update of an account where Description is set by the client
  • SetDescriptionToOwnerOnCreate:
    Test that the description is set correctly on create of an account according to the initial ownership.
    Here we are using the ctx.AddEntity to add the systemuser to the context, because this test is actually using the Kipon.Xrm.ServiceAPI.INamingService std. implementation to get the name of the user. The reason we are using ["fullname"] = value in the initialization of the systemuser is, that fullname is the primary attribute name of the systemuser, but it is readonly on the strongly typed entity, because you must set firstname and lastname. To overcome this, the unittest simple assign the value directly from the schemaname.
    ctx.OnPre is assigned a delegate method. This method is called by the kipon.solid.plugin.fake framework AFTER the onpre event has been called. If you add an On[State] event to the ctx, where the plugin does not have at least one method that listen for that event, it is considered an error, and the unit test will fail. In this example we hook into OnPre, and write the unit test in there where we ensure that the Description of the account entity has been set according to the rule
  • SetDescriptionToOwnerOnUpdate:
    Finally we create the corresponding test for update ownership on account. The main difference is, that we must add the preimage of the account to the context before calling the ctx.Update() method. As you can see, you just create a simple instance of the account and add it to the context. You do not need to know have that maps into preimages, mergedimages etc. the Kipon.Solid.Plugin.Fake framework know all about that.

© Kipon ApS 2020, 2021, 2022. All content on the page is the property of Kipon ApS. Any republish or copy of this content is a violation. The content of this site is NOT open source, and cannot be copied, republished or used in any context without explcit permission from the owner.