Skip to content

Course material

This is the material for the Developing with PIM course. Please make sure you have attended the Using PIM course and/or read the PIM concept article and PIM developer overview article

SDK

The NuGet package (Bizzkit.Sdk.Pim) is an automated swagger client that includes a factory client for easy authentication. See Swagger UI for reference.

API

The Bizzkit PIM API contains methods related to products, hierarchies, attributes, etc.

  • AttributeGroups
  • AttributeOrders
  • Attributes
  • Brands
  • Channels
  • ExternalBulkOperations
  • GlobalLists
  • Markets
  • ProductCategories
  • ProductHierarchies
  • Products
  • ResolvedViews
  • Segmentations
  • Tasks
  • TranslationCultures

and is more or less self-explanatory.

Frontend views

A frontend view is a simplified view of some PIM entity that is intended for consumption in an external system such as a website frontend. By simplified we mean flattened and resolved such that a view contains information that is targeted to one specific segmentation.

PIM has the following views:

  • Published product views
  • Product hierarchy views
  • Attribute views
  • Attribute predefined values views
  • Brand views
  • Attribute group views
  • Global list views

Use the Swagger UI or VS to get an overview of the request- and response models.

Lifecycle of views

First and foremost frontend views for some entity only exist for a specific segmentation if the segmentation has its Auto publish to frontend flag set to true. This also means that if the flag is disabled, all views related to that segmentation will be deleted. So, under the assumption that the flag is enabled, then views are created, updated and deleted automatically due to internal events inside PIM.

In general there’s no need to manually manage or rebuild views, however PIM does allow rebuilding the views on demand if this is required for some reason. That is done in the UI under Administration → Frontend Models.

Managing views

The managing of views happens in a recurring job in PIM job runner Hangfire. The name of the job is WatchAllGeneratedViewsAndModels and it creates, updates, and deletes a batch of views each time it runs. If there’s still work left to do, it enqueues itself immediately to run again and build another batch. This goes on until all views are built. After that it runs at a lower frequency until it detects work to be done again, and it all starts over.

Importing and exporting

PIM has the ability to import and export products, brands, global lists, product hierarchies, attributes, etc., from a csv-format. See CSV specification for a general reference.

API

The PIM API consists of several methods related to importing and exporting data - like

  • Attributes/Import
  • Brands/Import
  • Global lists/import
  • Product hierarchies/import
  • Products import
  • Products export

Response

An API call to import related methods will return a JSON response with information about the task and a result URL - like:

1
2
3
4
5
{
  "taskId": "9c2e8895-6e1f-4848-ad60-0af45f1fba3b",
  "statusUri": "/tasks/9c2e8895-6e1f-4848-ad60-0af45f1fba3b/status",
  "resultUri": "/tasks/9c2e8895-6e1f-4848-ad60-0af45f1fba3b/result"
}

You can use the GetStatus and GetResult methods to get the status of the import. See Swagger UI for more information about methods and models.

Webhooks

Internally in PIM domain events are being raised when product catalog items and other entities are created, updated, deleted, etc. A subset of these events can be subscribed to from external applications via Webhooks.

In order to subscribe to events, you have to register one or more webhook URLs in PIM. This is done either via the API or in the UI under Administration → Webhooks. The secret must be a string of at least 16 characters. It’s the value for the IssuerSigningKey of a JWT token. The same value should be used on the receiving end of the webhook.

If you prefer using the API instead of the UI that’s possible in the domain-events resource. Use the PUT operation. URL and secret previously mentioned are specified in the request body.

Payload

PIM will send a request to the web hook in a simple format (see also UI):

1
2
3
{
  "UniqueName": "Test"
}

External bulk operations

External bulk operations are an extension point in PIM, enabling you to perform custom operations or jobs on the product catalog which are run on the customer solution side. External bulk operations are triggered the same way as the built-in bulk operations already provided by PIM. This feature thus enables you to develop your own third-party integrations, when your specific needs are not covered by the built-in bulk operations.

You can set up an external bulk operation in Settings → External Bulk Operations and execute an operation through Products → Operations.

External bulk operations

Payload

When calling your customer solution, PIM makes a POST request with a JSON body according to the following specification:

1
2
3
4
ExternalBulkOperationWebhookPayload {
  operationName*          string
  productUniqueNames*     [string]
}

The fields of this body are used as:

  • operationName is the unique system name of the external bulk operation and is used to distinguish external bulk operations from one another when sharing a customer solution URL.
  • productUniqueNames is an array of the unique names of the products that were matched by the product catalog filter when triggering the external bulk operation from the dashboard.

Securing request

When creating an external bulk operation, a symmetric secret is generated for you and presented to you only once. This secret should be known only to PIM and the customer solution. After creation, there is no way to view the secret again via the dashboard or the public API. The secret is encrypted and sent to the customer solution in the HTTP headers on every request.

UI extension frame sets

In PIM you can extend the UI through Extensions Frame Sets. It provides a way to call an external URL with parameters, and display the result internally or externally.

UI Extension

You can extend the UI in the main tab

UI Extension

A click on the extension button with call a page (or several pages) with information about dimensions, segmentation, user ID, etc.

You can also extend the UI in the product tab

UI Extension

The link under the Product Administration will call a page (or several pages) with information about dimensions, segmentation, product ID, unique name, user ID, etc.

Examples

Based on the Getting-started console application here is a few examples in a helper class you can add to a project as PimHelper.cs.

using Bizzkit.Sdk.Pim;
using System.Text;

// WARNING: This is DEMO code and is not intended for production use.
// Please review and modify before using in any production environment.
namespace Bizzkit.Academy
{
    public static class PimHelper
    {
        public static async Task<Guid> CreateGlobalList(string listName, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            GlobalListCreateRequestModel request = new GlobalListCreateRequestModel()
            {
                SystemName = listName,
                ItemPimTypeName = "PString",
                UniqueValues = false
            };
            var responseGlobalLists_CreateAsync = await client.GlobalLists_CreateAsync(request);

            List<GlobalListItemsCreateRequestModel> items = new List<GlobalListItemsCreateRequestModel>();

            for (int i = 0; i < 10; i++)
            {
                items.Add(new GlobalListItemsCreateRequestModel
                {
                    SystemName = "ITEM" + i,
                    TemporaryIdentifier = Guid.NewGuid(),
                    Value = new PimTypeModel() { PimTypeName = "PString", ValueOrDefaultValue = new PimValueModel { Value = "Item #" + i } }
                });
            }
            var responseGlobalLists_Items_CreateAsync = await client.GlobalLists_Items_CreateAsync(responseGlobalLists_CreateAsync, items);
            return responseGlobalLists_CreateAsync;
        }

        public static async Task<Guid> GetAttributeId(string systemName, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            return (await client.Attributes_ListAsync(1, 1000)).Items.First(i => i.SystemName == systemName).AttributeId;
        }

        public static async Task<BrandListResponseModel> GetBrands(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            BrandListResponseModel result = await client.Brands_ListAsync(1, 1000);
            return result;
        }

        public static async Task<ChannelListResponseModel> GetChannels(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            return await client.Channels_ListAsync(1, 100);
        }

        public static async Task<IEnumerable<ProductViewWrapper>> GetFrontEndModel(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            ProductsViewSearchModel productsViewSearchModel = new ProductsViewSearchModel
            {
                PageNumber = 1,
                PageSize = 1000, // careful
                SegmentationId = null, // null = all
                UniqueNames = null,    // set to array if needed
            };
            IEnumerable<ProductViewWrapper> result = await client.ResolvedViews_Products_SearchAsync(productsViewSearchModel);
            return result;
        }

        // Warning - getting all items!!
        public static async Task<GlobalListsListResponseModel> GetGlobalLists(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            GlobalListsListResponseModel result = await client.GlobalLists_ListAsync(1, 1000);
            return result;
        }

        public static async Task<SegmentationListResponseModel> GetSegmentations(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            return await client.Segmentations_ListAsync(1, 100);
        }

        public static async Task<ListTranslationCulturesResult> GetTranslationCultures(DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            return await client.TranslationCultures_ListAsync(1, 100);
        }

        public static async Task<(EnqueuedTaskModel, StatusDescriptor)> ImportCsvFromFile(string file, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            string csv = System.IO.File.ReadAllText(file);
            return await ImportCsvFromString(csv, config);
        }

        public static async Task<(EnqueuedTaskModel, StatusDescriptor)> ImportCsvFromString(string content, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            byte[] byteArray = System.Text.Encoding.UTF8.GetBytes(content);
            MemoryStream stream = new MemoryStream(byteArray);
            FileParameter fileParameter = new FileParameter(stream);
            EnqueuedTaskModel response = await client.Products_Bundles_ImportFromCsvAsync(100, null, ProductBundlesImportJobPriority.Medium, fileParameter);
            StatusDescriptor status;
            int tryNo = 1;
            do
            {
                status = await client.Tasks_GetStatusAsync(response.TaskId);
                await Task.Delay(1000);
                if (tryNo > 25)
                    throw new ApplicationException("Something is wrong");
            } while (status.State != StatusDescriptorState.Finished);
            return (response, status);
        }

        public static async Task ImportProducts(string file, DomainConfig config)
        {
            string content = System.IO.File.ReadAllText(file);
            var client = await BizzkitHelper.GetPimClientFactory(config);
            byte[] byteArray = Encoding.UTF8.GetBytes(content);
            MemoryStream stream = new(byteArray);
            FileParameter fileParameter = new(stream);
            var result = await client.Products_ImportFromCsvAsync(
                batchSize: 50,
                csvFile: fileParameter,
                includedAttributes: ""
                );
            await WaitForResult(result, "Importing products from " + file, config);
        }

        public static async Task ImportProductsFromString(List<string> content, DomainConfig config)
        {
            if (content == null || content.Count == 0)
                return;
            foreach (var item in content)
                await ImportProductsFromString(item, config);
        }

        public static async Task ImportProductsFromString(string content, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            byte[] byteArray = Encoding.UTF8.GetBytes(content);
            MemoryStream stream = new(byteArray);
            FileParameter fileParameter = new(stream);
            var result = await client.Products_ImportFromCsvAsync(
                batchSize: 50,
                csvFile: fileParameter,
                includedAttributes: ""
                );
            await WaitForResult(result, "Importing products from content...", config);
        }

        public static async Task<IEnumerable<ProductViewWrapper>> SearchProducts(int segmentationId, DomainConfig config, IEnumerable<string>? uniqueNames = null)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            return await client.ResolvedViews_Products_SearchAsync(new ProductsViewSearchModel
            {
                PageNumber = 1,
                PageSize = 1000,
                SegmentationId = segmentationId,
                UniqueNames = uniqueNames
            });
        }

        public static async Task WaitForResult(EnqueuedTaskModel enqueuedTaskModel, string txt, DomainConfig config)
        {
            var client = await BizzkitHelper.GetPimClientFactory(config);
            await ExecuteWithIntervalUntilConditionAsync(async () =>
            {
                var result = await client.Tasks_GetStatusAsync(enqueuedTaskModel.TaskId);
                if (result.State == StatusDescriptorState.Failed)
                {
                    Console.WriteLine("Task failed: " + result.ErrorMessage);
                    throw new Exception("Task failed: " + txt + " " + result.ErrorMessage);
                }
                return result.State == StatusDescriptorState.Finished;
            }, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(120), txt);
        }

        private static async Task ExecuteWithIntervalUntilConditionAsync(
                                                                                    Func<Task<bool>> condition,
            TimeSpan interval,
            TimeSpan timeout,
            string logText)
        {
            var startTime = DateTime.UtcNow;
            while (true)
            {
                if (await condition())
                {
                    break;
                }
                var to = timeout.TotalSeconds - (DateTime.UtcNow - startTime).TotalSeconds;
                Console.WriteLine($"{logText} - {interval.TotalSeconds}s ({to.ToString("0")}s left)");
                if (DateTime.UtcNow - startTime > timeout)
                {
                    throw new TimeoutException("Operation timed out.");
                }

                await Task.Delay(interval);
            }
        }
    }
}