Dynamics CRM CE: TDD Plugin Development in Action – Part 2

For the second part of this blog post series, we will create a more advanced scenario operation. We will create business logic for the SalesOrder to calculate total orders (SalesOrder.Amount and Sum of SalesOrderDetail.Quantity) per Customer in 1 month (based on SalesOrder.SubmitDate) with the status of the Order is Invoiced. Remember, we can have multiple ways to solve this scenario. But for demonstration purposes, I will do the calculation in the Post-Operation stage where the targeted data (SalesOrder) is already final. I will skip all the preparation of the projects (You still can refer to this post for the structure and NuGet packages that I use).

Even though the scenario is simple, but technically we need to breakdown the steps required to do this operation.

CreateOrderSummary Scenario

The first scenario we will do is creating new_ordersummary based on the query that we retrieve from the database. The test case will be like this:

using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Niam.XRM.Framework;
using Niam.XRM.Framework.TestHelper;
namespace OrderSummary.Plugins.Tests
{
    [TestClass]
    public class OnCalculateTotalTests
    {
        [TestMethod]
        public void OnUpdate_CreateOrderSummary_ShouldValid()
        {
            var transactionDate = new DateTime(2020, 7, 2).ToUniversalTime();
            var profile = new Contact { Id = Guid.NewGuid() }.
                Set(e => e.FullName, "Temmy Raharjo");
            var order = new SalesOrder { Id = Guid.NewGuid() }
                .Set(e => e.SubmitDate, transactionDate)
                .Set(e => e.CustomerId, profile.ToEntityReference())
                .Set(e => e.TotalAmount, 500)
                .Set(e => e.StatusCode, SalesOrder.Options.StatusCode.Invoiced);
            var orderDetail1 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e=>e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 5);
            var orderDetail2 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e => e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 3);
            var test = new TestEvent<SalesOrder>(profile, order, orderDetail1, orderDetail2);
            test.CreateEventCommand<OnCalculateTotal>(order);
            Assert.IsTrue(test.Db.Event.Created.Any());
            var orderSummary = test.Db.Event.Created[0].ToEntity<new_ordersummary>();
            Assert.AreEqual("Order 202007-Temmy Raharjo", orderSummary.Get(e => e.new_summaryname));
            Assert.AreEqual(500, orderSummary.GetValue(e => e.new_totalamount));
            Assert.AreEqual(8, orderSummary.GetValue(e => e.new_totalqty));
        }
    }
}

Then the implementation of the code will be like this:

using System;
using System.Linq;
using Microsoft.Xrm.Sdk.Query;
using Niam.XRM.Framework;
using Niam.XRM.Framework.Data;
using Niam.XRM.Framework.Interfaces.Plugin;
using Niam.XRM.Framework.Plugin;
namespace OrderSummary.Plugins
{
    public class OnCalculateTotal : OperationBase<SalesOrder>
    {
        class DateSetup
        {
            public DateTime StartDate { get; set; }
            public DateTime EndDate { get; set; }
        }
        public OnCalculateTotal(ITransactionContext<SalesOrder> context) : base(context)
        {
        }
        protected override void HandleExecute()
        {
            var customerRef = Get(e => e.CustomerId);
            var transactionDate = Get(e => e.SubmitDate);
            var valid = Get(e => e.StatusCode).
                Equal(SalesOrder.Options.StatusCode.Invoiced) && customerRef != null && transactionDate.HasValue;
            if (!valid) return;
            var dateSetup = GetDateSetup(transactionDate.Value);
            var orderData = GetOrders(customerRef.Id, dateSetup.StartDate, dateSetup.EndDate).
                GroupBy(order => new { OrderId = order.Id, TotalAmount = order.GetValue(e => e.TotalAmount) }).
                Select(group =>
                {
                    var totalAmount = group.Key.TotalAmount;
                    var totalQty = group.Sum(order =>
                    {
                        var salesOrderDetail = order.GetAliasedEntity<SalesOrderDetail>("sod");
                        return salesOrderDetail.GetValue(e => e.Quantity);
                    });
                    return new { TotalAmount = totalAmount, TotalQty = totalQty };
                }).ToArray();
            var customerName = Service.GetReferenceName<Contact>(customerRef);
            var summaryName = string.Format("Order {0}-{1}", transactionDate.Value.ToString("yyyyMM"), customerName);
            var orderSummary = new new_ordersummary()
                .Set(e => e.new_customerid, customerRef)
                .Set(e => e.new_summaryname, summaryName)
                .Set(e => e.new_totalamount, orderData.Sum(e => e.TotalAmount))
                .Set(e => e.new_totalqty, (int)orderData.Sum(e => e.TotalQty));
            Service.Create(orderSummary);
        }
        private DateSetup GetDateSetup(DateTime value)
        {
            var startDate = new DateTime(value.Year, value.Month, 1);
            var endDate = startDate.AddMonths(1).AddSeconds(-1);
            return new DateSetup { StartDate = startDate, EndDate = endDate };
        }
        private SalesOrder[] GetOrders(Guid customerId, DateTime startDate, DateTime endDate)
        {
            var query = new QueryExpression(SalesOrder.EntityLogicalName)
            {
                ColumnSet = new ColumnSet<SalesOrder>(e => e.TotalAmount)
            };
            query.Criteria.AddCondition<SalesOrder>(e => e.CustomerId,
                ConditionOperator.Equal, customerId);
            query.Criteria.AddCondition<SalesOrder>(e => e.SubmitDate,
                ConditionOperator.Between, startDate, endDate);
            query.Criteria.AddCondition<SalesOrder>(e => e.StatusCode,
                ConditionOperator.Equal,
                (int)SalesOrder.Options.StatusCode.Invoiced);
            var salesOrderDetailLink = query.AddLink<SalesOrder,
                SalesOrderDetail>(e => e.Id, e => e.SalesOrderId);
            salesOrderDetailLink.Columns = new ColumnSet<SalesOrderDetail>(e => e.Quantity);
            salesOrderDetailLink.EntityAlias = "sod";
            var result = Service.RetrieveMultiple(query);
            return result.Entities.Any() ? result.Entities.Select(e => e.ToEntity<SalesOrder>()).ToArray()
                : new SalesOrder[] { };
        }
    }
}

If you run again the test, you will find the test pass (green) for this scenario. The complexity in code above is on getting calculation of Total Amount (SalesOrder.TotalAmount) and Quantity (SalesOrderDetail.Quantity). So the easiest way is to group the data based on the OrderId and TotalAmount, then Sum the SalesOrderDetail.Quantity.

  var orderData = GetOrders(customerRef.Id, dateSetup.StartDate, dateSetup.EndDate).
                GroupBy(order => new { OrderId = order.Id, TotalAmount = order.GetValue(e => e.TotalAmount) }).
                Select(group =>
                {
                    var totalAmount = group.Key.TotalAmount;
                    var totalQty = group.Sum(order =>
                    {
                        var salesOrderDetail = order.GetAliasedEntity<SalesOrderDetail>("sod");
                        return salesOrderDetail.GetValue(e => e.Quantity);
                    });
                    return new { TotalAmount = totalAmount, TotalQty = totalQty };
                }).ToArray();

The framework had a helper function to cast all the AliasedValue to become an Entity result (GetAliasedEntity) so Developer is easier to get the value. Below is the sample of the code if you using conventional way:

var quantityAliasedValue = order.Get<AliasedValue>("sod." + Helper.Name<SalesOrderDetail>(e => e.Quantity));
                        return ((decimal?)quantityAliasedValue.Value).GetValueOrDefault();

Then as you know, Customer datatype can have Account/Contact Entity as the reference. For that purpose, I will install xUnit and xUnitRunner because it provides cleaner API and our tests will be much better. For the test it changed to below code:

using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Niam.XRM.Framework;
using Niam.XRM.Framework.TestHelper;
using Xunit;
namespace OrderSummary.Plugins.Tests
{
    public class OnCalculateTotalTests
    {
        [Theory]
        [InlineData(Account.EntityLogicalName)]
        [InlineData(Contact.EntityLogicalName)]
        public void OnUpdate_CreateOrderSummary_ShouldValid(string customerLogicalName)
        {
            var transactionDate = new DateTime(2020, 7, 2).ToUniversalTime();
            var primaryFieldName = customerLogicalName == Account.EntityLogicalName ?
                Helper.Name<Account>(e => e.Name) : Helper.Name<Contact>(e => e.FullName);
            var customer = new Entity(customerLogicalName,
                Guid.NewGuid()).
                Set(primaryFieldName, "Temmy Raharjo");
            var order = new SalesOrder { Id = Guid.NewGuid() }
                .Set(e => e.SubmitDate, transactionDate)
                .Set(e => e.CustomerId, customer.ToEntityReference())
                .Set(e => e.TotalAmount, 500)
                .Set(e => e.StatusCode, SalesOrder.Options.StatusCode.Invoiced);
            var orderDetail1 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e => e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 5);
            var orderDetail2 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e => e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 3);
            var test = new TestEvent<SalesOrder>(customer, order, orderDetail1, orderDetail2);
            test.CreateEventCommand<OnCalculateTotal>(order);
            Assert.True(test.Db.Event.Created.Any());
            var orderSummary = test.Db.Event.Created[0].ToEntity<new_ordersummary>();
            Assert.Equal("Order 202007-Temmy Raharjo", orderSummary.Get(e => e.new_summaryname));
            Assert.Equal(500, orderSummary.GetValue(e => e.new_totalamount));
            Assert.Equal(8, orderSummary.GetValue(e => e.new_totalqty));
        }
    }
}

As what we know (Test Driven Development Process: Red – Green – Refactor), we need to create the test guide us. So we can run the test again and below picture should appear in your screen:

Then the implementation will be like this:

using System;
using System.Linq;
using Microsoft.Xrm.Sdk.Query;
using Niam.XRM.Framework;
using Niam.XRM.Framework.Data;
using Niam.XRM.Framework.Interfaces.Plugin;
using Niam.XRM.Framework.Plugin;
namespace OrderSummary.Plugins
{
    public class OnCalculateTotal : OperationBase<SalesOrder>
    {
        class DateSetup
        {
            public DateTime StartDate { get; set; }
            public DateTime EndDate { get; set; }
        }
        public OnCalculateTotal(ITransactionContext<SalesOrder> context) : base(context)
        {
        }
        protected override void HandleExecute()
        {
            var customerRef = Get(e => e.CustomerId);
            var transactionDate = Get(e => e.SubmitDate);
            var valid = Get(e => e.StatusCode).
                Equal(SalesOrder.Options.StatusCode.Invoiced) && customerRef != null && transactionDate.HasValue;
            if (!valid) return;
            var dateSetup = GetDateSetup(transactionDate.Value);
            var orderData = GetOrders(customerRef.Id, dateSetup.StartDate, dateSetup.EndDate).
                GroupBy(order => new { OrderId = order.Id, TotalAmount = order.GetValue(e => e.TotalAmount) }).
                Select(group =>
                {
                    var totalAmount = group.Key.TotalAmount;
                    var totalQty = group.Sum(order =>
                    {
                        var salesOrderDetail = order.GetAliasedEntity<SalesOrderDetail>("sod");
                        return salesOrderDetail.GetValue(e => e.Quantity);
                    });
                    return new { TotalAmount = totalAmount, TotalQty = totalQty };
                }).ToArray();
            var customerName = customerRef.LogicalName == Account.EntityLogicalName ?
                Service.GetReferenceName<Account>(customerRef) :
                Service.GetReferenceName<Contact>(customerRef);
            var summaryName = string.Format("Order {0}-{1}", transactionDate.Value.ToString("yyyyMM"), customerName);
            var orderSummary = new new_ordersummary()
                .Set(e => e.new_customerid, customerRef)
                .Set(e => e.new_summaryname, summaryName)
                .Set(e => e.new_totalamount, orderData.Sum(e => e.TotalAmount))
                .Set(e => e.new_totalqty, (int)orderData.Sum(e => e.TotalQty));
            Service.Create(orderSummary);
        }
        private DateSetup GetDateSetup(DateTime value)
        {
            var startDate = new DateTime(value.Year, value.Month, 1);
            var endDate = startDate.AddMonths(1).AddSeconds(-1);
            return new DateSetup { StartDate = startDate, EndDate = endDate };
        }
        private SalesOrder[] GetOrders(Guid customerId, DateTime startDate, DateTime endDate)
        {
            var query = new QueryExpression(SalesOrder.EntityLogicalName)
            {
                ColumnSet = new ColumnSet<SalesOrder>(e => e.TotalAmount)
            };
            query.Criteria.AddCondition<SalesOrder>(e => e.CustomerId,
                ConditionOperator.Equal, customerId);
            query.Criteria.AddCondition<SalesOrder>(e => e.SubmitDate,
                ConditionOperator.Between, startDate, endDate);
            query.Criteria.AddCondition<SalesOrder>(e => e.StatusCode,
                ConditionOperator.Equal,
                (int)SalesOrder.Options.StatusCode.Invoiced);
            var salesOrderDetailLink = query.AddLink<SalesOrder,
                SalesOrderDetail>(e => e.Id, e => e.SalesOrderId);
            salesOrderDetailLink.Columns = new ColumnSet<SalesOrderDetail>(e => e.Quantity);
            salesOrderDetailLink.EntityAlias = "sod";
            var result = Service.RetrieveMultiple(query);
            return result.Entities.Any() ? result.Entities.Select(e => e.ToEntity<SalesOrder>()).ToArray()
                : new SalesOrder[] { };
        }
    }
}

UpdateOrderSummary Scenario

We will add the capability to get the Order Summary if exists. The easiest way to achieve this is through new_ordersummary.new_summaryname because we have the naming pattern already.

The test scenario will be like below code (PS: I’m only put the new scenario):

using System;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Niam.XRM.Framework;
using Niam.XRM.Framework.TestHelper;
using Xunit;
namespace OrderSummary.Plugins.Tests
{
    public class OnCalculateTotalTests
    {
        [Theory]
        [InlineData(Account.EntityLogicalName)]
        [InlineData(Contact.EntityLogicalName)]
        public void OnUpdate_CreateOrderSummary_ShouldValid(string customerLogicalName)
        {
            // Same like code above!
        }
        [Fact]
        public void OnUpdate_UpdateOrderSummary_ShouldValid()
        {
            var transactionDate = new DateTime(2020, 7, 2).ToUniversalTime();
            var customer = new Account { Id = Guid.NewGuid() }.
                Set(e => e.Name, "Temmy Raharjo");
            var order = new SalesOrder { Id = Guid.NewGuid() }
                .Set(e => e.SubmitDate, transactionDate)
                .Set(e => e.CustomerId, customer.ToEntityReference())
                .Set(e => e.TotalAmount, 500)
                .Set(e => e.StatusCode, SalesOrder.Options.StatusCode.Invoiced);
            var orderDetail1 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e => e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 5);
            var orderDetail2 = new SalesOrderDetail { Id = Guid.NewGuid() }
                .Set(e => e.SalesOrderId, order.ToEntityReference())
                .Set(e => e.Quantity, 3);
            var orderSummaryData = new new_ordersummary { Id = Guid.NewGuid() }
                .Set(e => e.new_summaryname, "Order 202007-Temmy Raharjo")
                .Set(e => e.statecode, new_ordersummary.Options.statecode.Active);
            var test = new TestEvent<SalesOrder>(customer, order, orderDetail1, orderDetail2, orderSummaryData);
            test.CreateEventCommand<OnCalculateTotal>(order);
            Assert.True(test.Db.Event.Updated.Any());
            var orderSummary = test.Db.Event.Updated[0].ToEntity<new_ordersummary>();
            Assert.Equal(orderSummaryData.Id, orderSummary.Id);
            Assert.Equal(500, orderSummary.GetValue(e => e.new_totalamount));
            Assert.Equal(8, orderSummary.GetValue(e => e.new_totalqty));
        }
    }
}

If you run the test, you will get an error (must error!). Then the implementation is:

using System;
using System.Linq;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using Niam.XRM.Framework;
using Niam.XRM.Framework.Data;
using Niam.XRM.Framework.Interfaces.Plugin;
using Niam.XRM.Framework.Plugin;
namespace OrderSummary.Plugins
{
    public class OnCalculateTotal : OperationBase<SalesOrder>
    {
        class DateSetup
        {
            public DateTime StartDate { get; set; }
            public DateTime EndDate { get; set; }
        }
        public OnCalculateTotal(ITransactionContext<SalesOrder> context) : base(context)
        {
        }
        protected override void HandleExecute()
        {
            var customerRef = Get(e => e.CustomerId);
            var transactionDate = Get(e => e.SubmitDate);
            var valid = Get(e => e.StatusCode).
                Equal(SalesOrder.Options.StatusCode.Invoiced) && customerRef != null && transactionDate.HasValue;
            if (!valid) return;
            var dateSetup = GetDateSetup(transactionDate.Value);
            var orderData = GetOrders(customerRef.Id, dateSetup.StartDate, dateSetup.EndDate).
                GroupBy(order => new { OrderId = order.Id, TotalAmount = order.GetValue(e => e.TotalAmount) }).
                Select(group =>
                {
                    var totalAmount = group.Key.TotalAmount;
                    var totalQty = group.Sum(order =>
                    {
                        var salesOrderDetail = order.GetAliasedEntity<SalesOrderDetail>("sod");
                        return salesOrderDetail.GetValue(e => e.Quantity);
                    });
                    return new { TotalAmount = totalAmount, TotalQty = totalQty };
                }).ToArray();
            var customerName = customerRef.LogicalName == Account.EntityLogicalName ?
                Service.GetReferenceName<Account>(customerRef) :
                Service.GetReferenceName<Contact>(customerRef);
            var summaryName = string.Format("Order {0}-{1}", transactionDate.Value.ToString("yyyyMM"), customerName);
            var orderSummary = GetOrderSummaryForCreateOrUpdate(summaryName)
                .Set(e => e.new_customerid, customerRef)
                .Set(e => e.new_totalamount, orderData.Sum(e => e.TotalAmount))
                .Set(e => e.new_totalqty, (int)orderData.Sum(e => e.TotalQty));
            if (orderSummary.Id == Guid.Empty)
            {
                Service.Create(orderSummary);
            }
            else
            {
                Service.Update(orderSummary);
            }
        }
        private new_ordersummary GetOrderSummaryForCreateOrUpdate(string summaryName)
        {
            var query = new QueryExpression(new_ordersummary.EntityLogicalName)
            {
                ColumnSet = new ColumnSet(false),
                TopCount = 1
            };
            query.Criteria.AddCondition<new_ordersummary>(e => e.new_summaryname,
                ConditionOperator.Equal, summaryName);
            query.Criteria.AddCondition<new_ordersummary>(e => e.statecode,
                ConditionOperator.Equal, (int)new_ordersummary.Options.statecode.Active);
            var result = Service.RetrieveMultiple(query);
            return result.Entities.Any() ? result.Entities[0].ToEntity<new_ordersummary>()
                : new new_ordersummary().Set(e => e.new_summaryname, summaryName);
        }
        private DateSetup GetDateSetup(DateTime value)
        {
            var startDate = new DateTime(value.Year, value.Month, 1);
            var endDate = startDate.AddMonths(1).AddSeconds(-1);
            return new DateSetup { StartDate = startDate, EndDate = endDate };
        }
        private SalesOrder[] GetOrders(Guid customerId, DateTime startDate, DateTime endDate)
        {
            var query = new QueryExpression(SalesOrder.EntityLogicalName)
            {
                ColumnSet = new ColumnSet<SalesOrder>(e => e.TotalAmount)
            };
            query.Criteria.AddCondition<SalesOrder>(e => e.CustomerId,
                ConditionOperator.Equal, customerId);
            query.Criteria.AddCondition<SalesOrder>(e => e.SubmitDate,
                ConditionOperator.Between, startDate, endDate);
            query.Criteria.AddCondition<SalesOrder>(e => e.StatusCode,
                ConditionOperator.Equal,
                (int)SalesOrder.Options.StatusCode.Invoiced);
            var salesOrderDetailLink = query.AddLink<SalesOrder,
                SalesOrderDetail>(e => e.Id, e => e.SalesOrderId);
            salesOrderDetailLink.Columns = new ColumnSet<SalesOrderDetail>(e => e.Quantity);
            salesOrderDetailLink.EntityAlias = "sod";
            var result = Service.RetrieveMultiple(query);
            return result.Entities.Any() ? result.Entities.Select(e => e.ToEntity<SalesOrder>()).ToArray()
                : new SalesOrder[] { };
        }
    }
}

Please take note in the GetOrderSummaryForCreateOrUpdate. I re-using the same result entity but I’m set the ColumnSet(false) to avoid not necessary attributes push to the System. The best and safest way is to create new Entity and set the Id as the Id you retrieve.

Create Plugin Step

using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Niam.XRM.Framework;
using Niam.XRM.Framework.Interfaces.Plugin;
using Niam.XRM.Framework.Interfaces.Plugin.Configurations;
using Niam.XRM.Framework.Plugin;
namespace OrderSummary.Plugins
{
    public class PostSalesOrderCreate : PluginBase<SalesOrder>, IPlugin
    {
        public PostSalesOrderCreate(string unsecure, string secure) : base(unsecure, secure)
        {
        }
        protected override void Configure(IPluginConfiguration<SalesOrder> config)
        {
            config.ColumnSet = new ColumnSet(true);
        }
        protected override void ExecuteCrmPlugin(IPluginContext<SalesOrder> context)
        {
            new OnCalculateTotal(context).Execute();
        }
    }
    public class PostSalesOrderUpdate : PluginBase<SalesOrder>, IPlugin
    {
        public PostSalesOrderUpdate(string unsecure, string secure) : base(unsecure, secure)
        {
        }
        protected override void Configure(IPluginConfiguration<SalesOrder> config)
        {
            config.ColumnSet = new ColumnSet(true);
        }
        protected override void ExecuteCrmPlugin(IPluginContext<SalesOrder> context)
        {
            if (context.Target.ContainsAny(e => e.StatusCode))
            {
                new OnCalculateTotal(context).Execute();
            }
        }
    }
}

On the above code, we set which Business Logic needs to run using context.Target.ContainsAny(“attributeName”). Using this way, we can efficiently run the code only if certain attributes being changed/filled.

Conclusion

In this blog-post, you can see how the code evolves. The final result might be not clean enough like the below code:

if (orderSummary.Id == Guid.Empty)
{
	Service.Create(orderSummary);
}
else
{
	Service.Update(orderSummary);
}

That code will be cleaner and more understandable if we create an Extension class like Service.CreateOrUpdate(orderSummary). But the point is now you already have The Test as your guidance. You can simply do refactor without worrying it will do any harm to your production data.

Happy Coding!

3 thoughts on “Dynamics CRM CE: TDD Plugin Development in Action – Part 2

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.