top of page
  • Hina Garg

Data Exchange B/w Sitecore XC and XP by using Service Proxy - Part 1

Updated: Oct 5, 2020

Hello,


Hope everyone is doing well! In this post I am going to explain how to send the information from Sitecore XC to Sitecore XP by using service proxy. People who are using the commerce engine to manage a Catalog and Sellable items might come across the need of sending the products, orders, and customer information to their storefront especially if they are building a custom storefront. We can use Service Proxy to fulfill this requirement. This activity requires following steps:


Objective: Create a rest service endpoint in commerce engine that will be consumed by a component in Sitecore XP to access the information of the products stored in a Catalog. This catalog is initialized by Sitecore commerce engine in the Commerce Authoring environment.

In this blog post I am going to explain the implementation of Step 1 and will cover the rest in later blog posts.


To explain the implementation approach, I am going to use below case scenario:


Case Scenario: Let us say that we have around 10,000 sellable items in our Catalog. The data structure of a sellable item is complex such that it has several variants as well as list components. We must identify and send information of the products that are linked to each other by a common value. For example, in the below diagram, there are products linked to each other by a common value. And the design of the product page in our Storefront is such that we have to display Product 1 then Product 2 and [Product 3, Product 4, Product 5, Product 6] grouped together in different sections. I am not going to provide full data structure of the product here but just few fields:





Product 1 is a sellable item with following properties and values:

ProductID = 1

ProductName = Product1

ProductDescription = ProductDesc1

CommonProductD = 1


Product 2 is a sellable item with following properties and values:

ProductID = 2

ProductName = Product2

ProductDescription = ProductDesc2

CommonProductID = 1


Product 3 is a sellable item with following properties and values:

ProductID = 3

ProductName = Product3

ProductDescription = ProductDesc3

CommonProductID = 2


Product 4 is a sellable item with following properties and values:

ProductID = 4

ProductName = Product4

ProductDescription = ProductDesc4

CommonProductID = 2


Product 5 is a sellable item with following properties and values:

ProductID = 5

ProductName = Product5

ProductDescription = ProductDesc5

CommonProductID = 2


Product 6 is a sellable item with following properties and values:

ProductID = 6

ProductName = Product6

ProductDescription = ProductDesc6

CommonProductID = 2


Implementation Approach:

  1. Sitecore Experience Commerce (XC) provides a Visual Studio extension (as part of the Sitecore.Commerce.Engine.SDK) that creates project templates that facilitate the creation of your custom Sitecore XC plugins.

  2. Use this VS extension to create a new plugin ex: Plugin.Example.Products. This plugin will have a folder structure like below image:

3. Let's remove the folders and the sample files that are not required for this exercise. We are just going to need below folders and we will add required code files in coming steps.


Folders List:

Commands

Controllers

Models

Pipelines

Blocks

Arguments

Let's keep ConfigureServiceApiBlock.cs and ConfigureSitecore.cs as well.


Models: Models are POCO classes that are reusable inside entities and components.


4. Create a model inside the Models folder as Product.cs.


Note: CommonProductID is provided as a payload of the request. So it is defined in the ProductArgument class and not here.

namespace Plugin.Example.Products
{
    /// <summary>
    /// Defines the model for a product.
    /// </summary>
    public class Product : Model
    {
        public Product()
        {
            this.ProductDescription = "";
            this.ProductID = "";
            this.ProductName = "";
        }
        public string ProductID { get; set; }
        public string ProductDescription { get; set; }
        public string ProductName { get; set; }
    }
}

5. Create another mode inside the same folder named as ProductsList.cs

BaseLevelProducts = Product 1 (CommonProductID = ProductID)

FirstLevelProducts = Product2 (CommonProductID = ProductID of Product 1)

SecondLevelProducts = Product 3, Product 4, Product 5, Product 6 (CommonProductID = ProductID of Product 2)

using Sitecore.Commerce.Core;
using System.Collections.Generic;

namespace Plugin.Example.Products
{
    public class ProductsList : Model
    {
        /// <summary>
        /// Defines lists of products.
        /// </summary>
        public ProductsList()
        {
            this.BaseLevelProduct = new Product();
            this.FirstLevelProducts = new List<Product>();
            this.SecondLevelProducts = new List<Product>();
        }
        public Product BaseLevelProduct { get; set; }

        public List<Product> FirstLevelProducts { get; set; }

        public List<Product> SecondLevelProducts { get; set; }


    }
}

Arguments: Arguments will be used by the Pipeline Blocks.


6. Create a class called as ProductArgument.cs



using Plugin.Example.Products.Models;
using Sitecore.Commerce.Core;

namespace Plugin.Example.Products.Pipelines.Arguments
{
    public class ProductArgument : PipelineArgument
    {
        public ProductArgument()
        {
            this.ProductDataSet = new ProductsList();           
        }

        public ProductsList ProductDataSet { get; set; }

        public string CommonProductID { get; set; }


    }
}

Blocks: Pipeline blocks are responsible for implementing behaviors, actions and business logic.


7. Add a new Block called as GetProductsPipelineBlock.cs



using Microsoft.Extensions.Logging;
using Plugin.Example.Products.Models;
using Plugin.Example.Products.Pipelines.Arguments;
using Plugin.Example.Schema.Product.Components;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;
using System;
using System.Linq;
using System.Threading.Tasks;


namespace Plugin.Example.Products.Pipelines.Blocks
{
    [PipelineDisplayName(ProductConstants.Pipelines.Blocks.GetProductPipelineBlock)]
    public class GetProductPipelineBlock : PipelineBlock<ProductArgument, ProductArgument, CommercePipelineExecutionContext>
    {

        private readonly CommerceCommander _commerceCommander;
        public GetProductPipelineBlock(CommerceCommander commerceCommander)
        {
            this._commerceCommander = commerceCommander;
        }

        public override async Task<ProductArgument> Run(ProductArgument arg, CommercePipelineExecutionContext context)
        {
            try
            {
                Condition.Requires<ProductArgument>(arg).IsNotNull<ProductArgument>($"{this.Name}: The argument cannot be null.");


                if (String.IsNullOrEmpty(arg.CommonProductID))
                {
                    //add error message to the request's output
                    await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().Error, null, null,
                        $"{this.Name}: there is no commonproductid provided.").ConfigureAwait(false);
                    return (ProductArgument)null;
                }

                // find all the products having same CommonProductID.
	            // assuming you have a component that defines the product schema 
                // fetch the product component -  GetComponent<ProductInfo>

                var _findEntitiesInListCommand = this._commerceCommander.Command<FindEntitiesInListCommand>();
                CommerceList<CommerceEntity> listOfSellableItems = await _findEntitiesInListCommand.Process(context.CommerceContext, type: typeof(CommerceEntity).ToString(), listName:CommerceEntity.ListName<SellableItem>(), skip:0, take:10000);
                var itemsWithCommonProductID = listOfSellableItems.Items.FindAll(s => s.GetComponent<ProductInfo>().CommonProductID.Equals(Convert.ToInt32(arg.CommonProductID)));

                // if the list is not empty iterate through it and set the properties of a  product.
                if(itemsWithCommonProductID != null && itemsWithCommonProductID.Any())
                {
                    foreach(var sellableItem in itemsWithCommonProductID)
                    {

                        var productComponent = sellableItem.GetComponent<ProductInfo>();
                    
                        Product product = new Product();
                        product.ProductID = productComponent.ProductID.ToString();
                        product.ProductDescription = productComponent.ProductDescription;
                        product.ProductName = productComponent.ProductName;

                        // Identify the base level product and the list of first level products.
                        if (productComponent.CommonProductID == productComponent.ProductID)
                        {
                            arg.ProductDataSet.BaseLevelProduct = product;
                            await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().Information, null, null,
                            $"{this.Name}: finish fetching the base level product -> [{product.ProductID}].").ConfigureAwait(false);
                        }
                        else
                        {
                            arg.ProductDataSet.FirstLevelProducts.Add(product);
                            await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().Information, null, null,
                            $"{this.Name}: finish fetching the first level product -> [{product.ProductID}].").ConfigureAwait(false);

                        }
                    }
                }
            }
            catch (Exception ex)
            {
                context.Logger.LogError(ex, $"GetProductPipelineBlock.Exception: Message={ex.Message}");
            }
            return arg;

        }
    }
}



8. Add another block called as GetSecondLevelProductsPipelineBlock.cs


This block will use the output of the first block as its input.



using Microsoft.Extensions.Logging;
using Plugin.Example.Products.Models;
using Plugin.Example.Products.Pipelines.Arguments;
using Plugin.Example.Schema.Product.Components;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Plugin.Example.Products.Pipelines.Blocks
{
    [PipelineDisplayName(ProductConstants.Pipelines.Blocks.GetSecondLevelProductsPipelineBlock)]
    public class GetSecondLevelProductsPipelineBlock : PipelineBlock<ProductArgument, ProductArgument, CommercePipelineExecutionContext>
    {

        private readonly CommerceCommander _commerceCommander;
        public GetSecondLevelProductsPipelineBlock(CommerceCommander commerceCommander)
        {
            this._commerceCommander = commerceCommander;
        }

        public override async Task<ProductArgument> Run(ProductArgument arg, CommercePipelineExecutionContext context)
        {
            try
            {
                Condition.Requires<ProductArgument>(arg).IsNotNull<ProductArgument>($"{this.Name}: The argument cannot be null.");


                if (String.IsNullOrEmpty(arg.CommonProductID))
                {
                    //add error message to the request's output
                    await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().Error, null, null,
                        $"{this.Name}: there is no commonproductid provided.").ConfigureAwait(false);
                    return (ProductArgument)null;
                }
                if (arg.ProductDataSet.FirstLevelProducts != null && arg.ProductDataSet.FirstLevelProducts.Any())
                {
                    // iterate through the list of first level  products to identify the list of second level  products.
                    foreach (var firstLevelItem in arg.ProductDataSet.FirstLevelProducts)
                    {
                        var _findEntitiesInListCommand = this._commerceCommander.Command<FindEntitiesInListCommand>();
                        CommerceList<CommerceEntity> listOfSellableItems = await _findEntitiesInListCommand.Process(context.CommerceContext, type: typeof(CommerceEntity).ToString(), listName: CommerceEntity.ListName<SellableItem>(), skip: 0, take: 10000);

                        var itemsWithCommonProductID = listOfSellableItems.Items.FindAll(s => s.GetComponent<ProductInfo>().CommonProductID.Equals(Convert.ToInt32(firstLevelItem.ProductID)));

                        // set the properties of second level products.
                        if (itemsWithCommonProductID != null && itemsWithCommonProductID.Any())
                        {
                            foreach (var sellableItem in itemsWithCommonProductID)
                            {
                                var productComponent = sellableItem.GetComponent<ProductInfo>();
                               

                                Product product = new Product();
                                product.ProductID = productComponent.ProductID.ToString();
                                product.ProductDescription = productComponent.Description;
                                product.ProductName = productComponent.ProductName;

                                
                                arg.ProductDataSet.SecondLevelProducts.Add(product);
                               
                                await context.CommerceContext.AddMessage(context.GetPolicy<KnownResultCodes>().Information, null, null,
                                $"{this.Name}: finish fetching the second level product -> [{product.ProductID}].").ConfigureAwait(false);                                
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                context.Logger.LogError(ex, $"GetSecondLevelProductsPipelineBlock.Exception: Message={ex.Message}");
            }
            return arg;

        }
    }
}

Pipelines: Pipelines act as containers for business logic. The pipeline class itself is typically lightweight and mainly expresses the definition of the pipeline itself.


9. Create a pipeline class called as IGetProductInformationPipeline.cs



using Plugin.Example.Products.Pipelines.Arguments;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;

namespace Plugin.Example.Products.Pipelines
{
    public interface IGetProductInformationPipeline: IPipeline<ProductArgument, ProductArgument, CommercePipelineExecutionContext>
    {
    }
}

10. Add another pipeline class known as GetProductInformationPipeline.cs



using Microsoft.Extensions.Logging;
using Plugin.Example.Products.Pipelines.Arguments;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Pipelines;

namespace Plugin.Example.Products.Pipelines
{
    /// <summary>
    /// create a new pipeline.
    /// </summary>
    public class GetProductInformationPipeline: CommercePipeline<ProductArgument, ProductArgument>, IGetProductInformationPipeline
    {
        public GetProductInformationPipeline(
           IPipelineConfiguration<IGetProductInformationPipeline> configuration,
           ILoggerFactory loggerFactory)
           : base(configuration, loggerFactory)
        {
        }
    }
}

Commands: We will define a command, and expose it through a REST API, we will also define a controller action for it. Controller classes are ODATA-compliant and based on a standard ASP.NET Core model-view-controller (MVC) pattern.


11. Create a command class called as GetProductInformationCommand.cs



using Plugin.Example.Products.Models;
using Plugin.Example.Products.Pipelines;
using Plugin.Example.Products.Pipelines.Arguments;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using System;
using System.Threading.Tasks;

namespace Plugin.Example.Products.Commands
{
    public class GetProductInformationCommand : CommerceCommand
    {
        private readonly IGetProductInformationPipeline _pipeline;

        public GetProductInformationCommand(
            IGetProductInformationPipeline getProductInformationPipeline,
            IServiceProvider serviceProvider)
            : base(serviceProvider)
        {
            //inject our pipeline to this command
            this._pipeline = getProductInformationPipeline;
        }
        public virtual async Task<ProductsList> Process(
            string commonProductID,
            CommerceContext commerceContext)
        {
            GetProductInformationCommand command = this;
            var productItemData = new ProductsList();
            //run the pipeline
            using (CommandActivity.Start(commerceContext, command))
            {
                await command.PerformTransaction(commerceContext, async () =>
                {
                    var PipelineContextOptions = commerceContext.PipelineContextOptions;
                    ProductArgument productArgument = await _pipeline.Run(
                        new ProductArgument()
                        {
                            CommonProductID = commonProductID
                        },
                        PipelineContextOptions).ConfigureAwait(false);


                    productItemData = productArgument.ProductDataSet;

                    commerceContext.AddModel(productArgument.ProductDataSet);

                }).ConfigureAwait(false);
            }

            return productItemData;
        }
    }
}

Controllers: We will expose the command created above by using below controller:


12. Create a new controller class called as CommandsController.cs



using System;
using System.Threading.Tasks;
using System.Web.Http.OData;
using Microsoft.AspNetCore.Mvc;
using Plugin.Example.Products.Commands;
using Sitecore.Commerce.Core;

namespace Plugin.Example.Products
{
    /// <inheritdoc />
    /// <summary>
    /// Defines a controller
    /// </summary>
    /// <seealso cref="T:Sitecore.Commerce.Core.CommerceController" />
    public class CommandsController : CommerceController
    {
        /// <inheritdoc />
        /// <summary>
        /// Initializes a new instance of the <see cref="T:Sitecore.Commerce.Plugin.Sample.CommandsController" /> class.
        /// </summary>
        /// <param name="serviceProvider">The service provider.</param>
        /// <param name="globalEnvironment">The global environment.</param>
        public CommandsController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment)
            : base(serviceProvider, globalEnvironment)
        {
        }

        /// <summary>
        /// Defines the action to get products information.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>A <see cref="IActionResult"/></returns>
        [HttpPut]
        [Route("GetProductInformation()")]
        public async Task<IActionResult> GetProductInformation([FromBody] ODataActionParameters value)
        {
            if (!value.ContainsKey("CommonProductID")
             )
                //raise and response the bad request error
                return (IActionResult)new BadRequestObjectResult((object)value);

            //parsing parameters
            string commonProductID = value["CommonProductID"].ToString();

            var command = this.Command<GetProductInformationCommand>();

            //execute the command and return the request's output
            var result = await command.Process(commonProductID, this.CurrentContext);

            return new ObjectResult(command);
        }
    }
}

13. Now let's update the ConfigureServiceApiBlock.cs



using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Builder;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;

namespace Plugin.Example.Products
{
    /// <summary>
    /// Defines a block which configures the OData model
    /// </summary>
    /// <seealso>
    ///     <cref>
    ///         Sitecore.Framework.Pipelines.PipelineBlock{Microsoft.AspNetCore.OData.Builder.ODataConventionModelBuilder,
    ///         Microsoft.AspNetCore.OData.Builder.ODataConventionModelBuilder,
    ///         Sitecore.Commerce.Core.CommercePipelineExecutionContext}
    ///     </cref>
    /// </seealso>
    [PipelineDisplayName("Plugin.Example.Products.ConfigureServiceApiBlock")]
    public class ConfigureServiceApiBlock : PipelineBlock<ODataConventionModelBuilder, ODataConventionModelBuilder, CommercePipelineExecutionContext>
    {
        /// <summary>
        /// The execute.
        /// </summary>
        /// <param name="arg">
        /// The argument.
        /// </param>
        /// <param name="context">
        /// The context.
        /// </param>
        /// <returns>
        /// The <see cref="ODataConventionModelBuilder"/>.
        /// </returns>
        public override Task<ODataConventionModelBuilder> Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
        {
            Condition.Requires(modelBuilder).IsNotNull($"{this.Name}: The argument cannot be null.");

            var GetProductInformationActionConfiguration = modelBuilder.Action("GetProductInformation");
            GetProductInformationActionConfiguration.Parameter<string>("CommonProductID");

            GetProductInformationActionConfiguration.ReturnsFromEntitySet<CommerceCommand>("Commands");

            return Task.FromResult(modelBuilder);
        }
    }
}

14. Finally, let's update the code for ConfigureSitecore.cs class.



using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Plugin.Example.Products.Pipelines;
using Plugin.Example.Products.Pipelines.Blocks;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Configuration;
using Sitecore.Framework.Pipelines.Definitions.Extensions;

namespace Plugin.Example.Products
{
    /// <summary>
    /// The configure sitecore class.
    /// </summary>
    public class ConfigureSitecore : IConfigureSitecore
    {
        /// <summary>
        /// The configure services.
        /// </summary>
        /// <param name="services">
        /// The services.
        /// </param>
        public void ConfigureServices(IServiceCollection services)
        {
            var assembly = Assembly.GetExecutingAssembly();
            services.RegisterAllPipelineBlocks(assembly);
            services.RegisterAllCommands(assembly);

            services.Sitecore().Pipelines(config => config
                //Configuring the API service for our engine plugin project
                .ConfigurePipeline<IConfigureServiceApiPipeline>(configure => configure.Add<ConfigureServiceApiBlock>())
                .AddPipeline<IGetProductInformationPipeline, GetProductInformationPipeline>(
                    configure =>
                    {
                        configure
                            .Add<GetProductProductPipelineBlock>()
                            .Add<GetSecondLevelProductProductsPipelineBlock>();
                    }));
        }
    }
}

Also, don't forget to create a constants class for defining product constants.


Testing: In order to test above code, add the plugin to your custom sitecore commerce engine project and deploy it to all the four roles: Authoring, Shops, Minions and Ops. Then in postman create a PUT request with the payload like below:



Once you hit send, it will call the action in your controller and you should be able to debug your code.


Hope this blog is helpful for the people who are trying to build a service in the commerce engine. In the second part of this blog I am going to explain the set up of Service Proxy which would enable us to consume the endpoint created in this exercise on the Sitecore XP side.


Thanks!!

182 views0 comments
bottom of page