Tuesday, May 22, 2018

Sitecore Experience Commerce - Service Proxy execute compilation error

When attempting to call my custom API in the commerce engine, I was getting a compilation error on the Execute method of a service proxy call.
Cannot convert from 'Microsoft.OData.Client.DataServiceActionQuerySingle<MyModel>' to 'Microsoft.OData.Client.DataServiceActionQuery<MyModel>' 
At first I thought it was a duplicate method across both the shops and ops proxy. But as it turns out, I was expecting a return type of collection and the proxy was expecting a single return object (hence the single at the end of DataServiceActionQuerySingle).

This was resolved by going back into the commerce engine where I configured the API and changing the return type from:
configuration.ReturnsFromEntitySet<ExtendedPricingDetail>("Api");
To:
 configuration.ReturnsCollectionFromEntitySet<ExtendedPricingDetail>("Api");
Then a re-generation of the service proxy and all was working as expected.

Monday, May 21, 2018

Sitecore Experience Commerce - Creating a custom API in the Commerce Engine

Due to a customization in how my Sitecore Experience Commerce implementation structured pricing, there was a requirement for a custom API which returned this additional data. In my post Drilling into storefront rendering code and commerce engine pipeline code, I dug down to the proxy executing the API GetBulkPrices. In this post I am going to create my own custom implementation GetExtendedBulkPrices.

The return model

First I needed to create a model, which I wanted this method to return.
using System;
using System.ComponentModel.DataAnnotations;

namespace Plugin.MyProject.PriceDetail.Model
{
    public class ExtendedPricingDetail
    {
        [Key]
        public string ProductId { get; set; }
        public Decimal OriginalPrice { get; set; }
        public int Flybuys { get; set; }
        public string PromotionCode { get; set; }
        public string PromotionText { get; set; }

        public ExtendedPricingDetail()
        {
            ProductId = string.Empty;
            OriginalPrice = 0.00M;
            Flybuys = 0;
            PromotionCode = string.Empty;
            PromotionText = string.Empty;
        }
    }
}
The key attribute was required as it would error otherwise.

The command

I needed a command for my custom API to call. A command will generally not contain any business logic and will simply be used to call a pipeline. This post won't go as deep as pipelines, however my empty commerce minion code sample post has a pipeline example.
using Plugin.MyProject.PriceDetail.Model;
using Plugin.MyProject.PriceDetail.Pipelines;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.Core.Commands;
using Sitecore.Commerce.Plugin.Catalog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Plugin.MyProject.PriceDetail.Commands
{
    public class GetExtendedBulkPricesCommand : CommerceCommand
    {
        private readonly IExtendedPricingDetailPipeline _pipeline;

        public GetExtendedBulkPricesCommand(IExtendedPricingDetailPipeline getSellableItemPipeline, IServiceProvider serviceProvider) : base(serviceProvider)
        {
            _pipeline = getSellableItemPipeline;
        }

        public async Task<IEnumerable<ExtendedPricingDetail>> Process(CommerceContext commerceContext, IEnumerable<string> itemIds)
        {
            using (CommandActivity.Start(commerceContext, this))
            {
                CommercePipelineExecutionContextOptions contextOptions = commerceContext.GetPipelineContextOptions();
                var items = new List<ExtendedPricingDetail>();

                foreach (var current in itemIds)
                {
                    ProductArgument productArgument = ProductArgument.FromItemId(current);
                    if (!productArgument.IsValid())
                    {
                        string str = await contextOptions.CommerceContext.AddMessage(commerceContext.GetPolicy<KnownResultCodes>().Error, "ItemIdIncorrectFormat", new object[1]
                        {
                            (object) current
                        }, string.Format("Expecting a CatalogId and a ProductId in the ItemId: {0}.", (object)current));
                    }
                    else
                    {
                        var priceDetail = await _pipeline.Run(productArgument, contextOptions.CommerceContext.GetPipelineContextOptions());
                       
                        if (priceDetail != null)
                        {
                            items.Add(priceDetail);
                        }
                    }
                }

                return items.AsEnumerable();
            }
        }
    }
}
As you can see, this example calls my custom pipeline IExtendedPricingDetailPipeline.

The API Controller

Now we need a controller with an action which can act as an API method, this code is a standard MVC example.
using Sitecore.Commerce.Core;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Web.Http.OData;
using Plugin.MyProject.PriceDetail.Commands;

namespace Plugin.MyProject.PriceDetail.Controller
{
    public class ApiController : CommerceController
    {
        public ApiController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment): base(serviceProvider, globalEnvironment)
        {
        }

        [HttpPut]
        [Route("GetExtendedBulkPrices()")]
        public async Task<IActionResult> GetExtendedBulkPrices([FromBody] ODataActionParameters value)
        {
            if (!ModelState.IsValid)
                return (IActionResult)new BadRequestObjectResult(ModelState);
            if (!value.ContainsKey("itemIds") || !(value["itemIds"] is JArray))
                return (IActionResult)new BadRequestObjectResult((object)value);
            JArray jarray = (JArray)value["itemIds"];

            var bulkCommand = Command<GetExtendedBulkPricesCommand>();
            var res = await bulkCommand.Process(this.CurrentContext, jarray != null ? jarray.ToObject<IEnumerable<string>>() : (IEnumerable<string>)null);

            return (IActionResult)new ObjectResult(res);
        }
    }
}

In this example we are calling this ApiController, which means it follows an example of a Shops API method. Much like the GetBulkPrices method, that means the Postman call URL for this API would be {{ServiceHost}}/{{ShopsApi}}/GetExtendedBulkPrices(). It takes in an input of a list of strings and will output my custom model defined above.

This simply calls my command (as defined above).

Registering Exerything

Inside the Configure Sitecore class for the given plugin which contains these newly added classes, they need to be configured. This is done as follows (and does not show the Pipeline configuration):
public void ConfigureServices(IServiceCollection services)
{
    var assembly = Assembly.GetExecutingAssembly();
    services.RegisterAllCommands(assembly);

    services.Sitecore().Pipelines(config =>
        config
            .ConfigurePipeline<IConfigureServiceApiPipeline>(c => 
            {
                c.Add<RegisterPricingApiBlock>();
            })
    );
}
There is a single method which will register all commands, and added is a reference to RegisterPricingApiBlock. This is where the API and it's added entities are registered/built up for the oData service layer (and therefore commerce proxy used from the front-end). The code for this class is:
using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Builder;
using Plugin.MyProject.PriceDetail.Model;
using Sitecore.Commerce.Core;
using Sitecore.Framework.Conditions;
using Sitecore.Framework.Pipelines;

namespace Plugin.MyProject.PriceDetail.Pipelines.Blocks
{
    class RegisterPricingApiBlock : PipelineBlock<ODataConventionModelBuilder, ODataConventionModelBuilder, CommercePipelineExecutionContext>
    {
        public RegisterPricingApiBlock() { }

        public override Task<ODataConventionModelBuilder> Run(ODataConventionModelBuilder modelBuilder, CommercePipelineExecutionContext context)
        {
            Condition.Requires(modelBuilder).IsNotNull($"{base.Name}: The argument can not be null");

            modelBuilder.AddEntityType(typeof(ExtendedPricingDetail));
            modelBuilder.EntitySet<ExtendedPricingDetail>("ExtendedPricingDetails");

            var configuration = modelBuilder.Action("GetExtendedBulkPrices");
            configuration.CollectionParameter<string>("itemIds");
            configuration.ReturnsCollectionFromEntitySet<ExtendedPricingDetail>("Api");

            return Task.FromResult(modelBuilder);
        }
    }
}
Here the entity (custom model) and API method have been registered, which now allows them to be called via Postman and accessed via the Service Proxy project.

Calling the API via Postman

The API call can be a copy of one of the existing examples that come with the SDK. In my case, as I was using the same input (string array) as the GetBulkPrices API call, I simply copied this and updated the action name.

Calling the API via Postman


Updating the Service Proxy

Rob Earlam, has a great explanation up about what the Service Proxy is. Basically it's used to be able to share models between the .NET Core commerce Engine and standard .NET Sitecore website. However as I have added a custom model, I will need this to re-generate at the Service Proxy level.

The prerequisite for this is to install the OData v4 Client Code Generator plugin into Visual Studio 2017.

Now you simply add the Sitecore.Commerce.ServiceProxy project to your solution and under connected services are the ops and shops services which have a right click option to update (as long as a commerce engine instance is listening on port 5000). There is a JSON configuration if required.

Regenerating the Service Proxy

Conclusion

This was a simple example of how a custom API endpoint can be added to the commerce engine to call custom code and return custom models. This code could be slightly modified/expanded to actually extend a given model in the commerce engine layer.

Sitecore Experience Commerce - The entity does not have a key defined

After creating a custom command, API and return entity, I was getting the following error when starting the commerce engine in debug mode.
The entity 'MyModel' does not have a key defined
at Microsoft.AspNetCore.OData.Builder.ODataModelBuilder.ValidateModel(IEdmModel model)
The fix was to add the [Key] attribute to one of the attributes of my model. This can be found under the System.ComponentModel.DataAnnotations model.
public class ExtendedPricingDetail
{
 [Key]
 public string ProductId { get; set; }
 public Decimal CurrentPrice { get; set; }
 public Decimal OriginalPrice { get; set; }
 public string PromotionCode { get; set; }
 public string PromotionText { get; set; }
 public bool OnSpecial { get; set; }

 public ExtendedPricingDetail()
 {
  ProductId = string.Empty;
  CurrentPrice = 0.00M;
  OriginalPrice = 0.00M;
  PromotionCode = string.Empty;
  PromotionText = string.Empty;
  OnSpecial = false;
 }
}

Sitecore Experience Commerce - Command won't resolve in commerce engine

I have a custom API in the commerce engine which was able to be called via Postman. However when debugging the API, it was unable to get a copy of my command. The line of code which was failing was:
var myCommand = Command<MyCustomCommand>();
This was returning null. What was missing that the command was not properly being registered, this is done in the  ConfigureSitecore class of the plugin which contains the command and is achieved with the following line of code:
services.RegisterAllCommands(assembly);
After this the command was able to be accessed and run correctly.

Thursday, May 10, 2018

Sitecore Experience Commerce - Brand facet is splitting text by spaces

During an implementation of a product list in Experience Commerce, I found that the brand facet was splitting the brand's text at the occurrence of spaces. So a brand of "My Brand" would appear as two separate facet items: "my" and "brand".

The field was set to "UN_TOKENIZED" which should have not allowed this to happen. Nevertheless, the fix is to change the fields returnType from "test" to "string". Then rebuild the web and master indexes for your Sitecore instance. Below is a patch file to make this change (for SOLR search provider).