First real blog post!
As I noted in the previous post, these posts are part-information and part-discovery journal of a sort. This means that my posts can get pretty long (this one certainly is). Click on the TL;DR link in the table of contents to scroll down to the, well, TL;DR version.
[toc]
Motivation
If you try using the Asset Importer for Commerce on an EPiServer Commerce version 8.X site, you will find that it does not work. I recently helped a customer resolve this issue and will share the solution. The customer was developing on the latest versions of CMS and Commerce (i.e. version 8.X.Y).
Introduction to Asset Importer
The Asset Importer is a command-line tool used for importing assets in EPiServer Commerce. Click here to download the package. The tool is in EPiServer.Business.Commerce.Tools.ImportAsset.zip.
The Problem
The tool that can be downloaded from EPiServer World is compatible with the following versions of the product:
CMS 7.19.1
Commerce 8.7.1
The customer could not get the tool to run because part of the initialization of the tool was using code from CMS 7.X that is incompatible with EPiServer CMS 8.
How to successfully run the Asset Importer for Commerce AND CMS 8
Prerequisite
- Have a working Commerce site for which to run the tool.
- Note the versions used for CMS and Commerce for your site.
Setting up the tool
The tool must be compiled with the matching versions of the Commerce site, and by extension, the CMS components. The easiest way to accomplish this is to apply NuGet packages.
- Install the Commerce Core package. Find the version of your Commerce site under Version History. I picked 8.12.0 arbitrarily. Run the following command in your Package Manager Console in Visual Studio to install the package.
Install-Package EPiServer.Commerce.Core -Version 8.12.0 - Next, update the CMS Core package to match the CMS version of your site. While EPiServer.Commerce.Core 8.12.0 has a dependency on EPiServer.CMS.Core 8.5.0, if you have updated the CMS package to a later version, your Asset Importer should match that version. If, for example, your CMS version is 8.9.0, you would run the following command:
Update-Package EPiServer.CMS.Core -Version 8.9.0
The dependency on EPiServer.Framework is automatically resolved correctly by EPiServer.CMS.Core.
Running the tool with code fixes
Initialization
Now we are ready to tackle the actual problem. Compile the project, and you will see this error three times:
Warning as Error: 'EPiServer.Configuration.EPiServerSection.ConfigurationInstance' is obsolete: 'Use ConfigurationSource.Instance = new FileConfigurationSource(value) to replace global configuration or use GlobalConfigurationManager to Load and Save configuration files'
That’s straightforward. Let’s make that change on all the place:
ConfigurationSource.Instance = new FileConfigurationSource(config);
Compile again, and there are no errors. You can run the tool in a separate command-line window and supply the arguments there, or you can supply the arguments in Visual Studio by: right-clicking on the project -> Properties -> Debug -> Start Options -> Command line arguments:
Refer to the documentation to determine the command parameters of the Asset importer. This blog, written for Commerce 7.5, is also useful for seeing what the tool accomplishes.
Unfortunately, we aren’t done yet. If you run the tool now, you will see the following error:
An unhandled exception of type 'EPiServer.Framework.Initialization.InitializationException' occurred in AssetImporter.exe
Assuming your assembly versions match the versions on your site, you will see this as the InnerException:
{"The assembly AssetImportTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null cannot be registered for module EPiServer.Commerce.Shell since it has already been registered by module App."}
The initialization largely happens in ~/Utils/SiteProxy.cs . In this case, the problem lies in all the code from line 38 to line 54. The solution is to simply delete that chunk of code to let the product take care of initialization without all the additional steps. The result is that the method InitializeEPiServer looks like this:
private void InitalizeEPiServer(string webConfigPath) { if (!File.Exists(webConfigPath)) { throw new ArgumentException("The configuration file '" + webConfigPath + "' does not exist", "webConfigPath"); } var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = webConfigPath }; var config = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); // Update use of obsolete method ConfigurationSource.Instance = new FileConfigurationSource(config); // Just let the product do its thing EPiServer.Framework.Initialization.InitializationModule.FrameworkInitialization(HostType.Installer); }
If you are testing with just one asset, for example, you will probably see this output:
Upload folder to CMS site... Association asset with entry... Updating Catalog Asset... 0 to 0 Updating Catalog Asset... 1 to 0 Updating Catalog Asset... 1 to 0 Updating Catalog Node Asset... 0
Adding file extensions
If you check your SKU, you will see that your asset was not uploaded. Why would that be? First, make sure that the file extensions of your assets are “valid”. By default, the tool looks for three extensions: .bmp, .jpg, and .png in the ImportAsset method in ~/Utils/CmsContentAssetImporter.cs. If, for example, you wanted to import a PDF as an asset for a SKU, you would add that extension like so:
FileInfo[] assetFiles = di.GetFiles("*.bmp", SearchOption.AllDirectories) .Union(di.GetFiles("*.jpg", SearchOption.AllDirectories)) .Union(di.GetFiles("*.png", SearchOption.AllDirectories)) .Union(di.GetFiles("*.pdf", SearchOption.AllDirectories)) // Add the extension for PDF. .ToArray();
OK, so now you are sure that the tool will accept your file as a valid asset to import. Let’s run the tool again. Here is the output:
Upload folder to CMS site... Association asset with entry... Updating Catalog Asset... 0 to 0 Updating Catalog Asset... 1 to 0 Updating Catalog Asset... 1 to 1 Updating Catalog Node Asset... 0
You’ll notice that the third “Updating Catalog Asset” line now displays “1 to 1” instead of “1 to 0”. Something definitely changed, but there is still no asset for your SKU. Also, you might be wondering, “Why three lines of ‘Updating Catalog Asset'”?
MappingEntryHelper.cs
Looking towards the bottom of the ImportAsset method, you will see some calls to static methods of the MappingEntryHelper class. This is a static class in the Mediachase.Commerce.Assets.Import namespace and Mediachase.Commerce assembly, and this is where the actual import takes place for our import tool. This is what is called by MappingHelper.AssociateAssetsWithProduct() from CmsContentAssetImporter.cs:
public static void AssociateAssetsWithProduct(IProgressMessenger progressMessenger) { // Using thread for update Catalog Associate Assets faster int c = entryMapping.Count(); int oneThirdIndex = c / 3; int twoThirdsIndex = oneThirdIndex * 2; Task task1 = Task.Factory.StartNew(() => AssociateAssetsWithProduct_thread(progressMessenger, 0, oneThirdIndex)); Task task2 = Task.Factory.StartNew(() => AssociateAssetsWithProduct_thread(progressMessenger, oneThirdIndex + 1, twoThirdsIndex)); Task task3 = Task.Factory.StartNew(() => AssociateAssetsWithProduct_thread(progressMessenger, twoThirdsIndex + 1, c)); Task.WaitAll(task1, task2, task3); }
So, the import is actually separated into three separate threads to speed things up. That makes sense, but why didn’t our test import of one asset succeed? The answer is clear when we examine the method that the above method is calling:
private static void AssociateAssetsWithProduct_thread(IProgressMessenger progressMessenger, int startIndex, int endIndex) { //... for (int i = startIndex; i < endIndex; i++) { // Code to associate asset with catalog entry } //... }
Do you see the problem here? The second parameter, startIndex, is not getting the right values for the second and third tasks. In our previous run, we had: (0,0), (1,0), and (1,1) as the start and end indices passed into this thread worker method, and this means that the code never gets to enter the for-loop. (If we had multiple assets to map to multiple SKUs, we would still be skipping over up to two mappings.)
Because this bug is in the EPiServer.Commerce.Core package, the ideal way to fix this would be to upgrade to a version that has this fix. At the time of writing this post, however, there isn’t a version with a fix available, so we will have to add two files to your Asset Importer project. Get them here. MappingEntryHelper.cs contains the bug fix, while EntryAssetsHelper.cs is just a necessary addition since MappingEntryHelper.cs calls some internal methods from the EntryAssetsHelper class. The Gist also contains CmsContentAssetImporter.cs and SiteProxy.cs.
Run the tool
We are finally ready to run the tool. The output should look like the following:
Upload folder to CMS site... Association asset with entry... Updating Catalog Asset... 0 to 0 Updating Catalog Asset... 0 to 0 Updating Catalog Asset... 0 to 1 Updating Catalog Node Asset... 0
Check your site to see that the asset has been successfully uploaded and associated with your SKU. You may need to recycle your application pool in order to see the update properly (this seems to be at least partly because the tool does not run inside a normal HttpContext).
Optional code to use while testing
When you are trying out the Asset Importer, you will probably end up running the tool multiple times and end up creating lots of duplicate and/or unwanted media content in your site and blobs in your blob storage location. Here is a bit of code to clean up that clutter. You can even add this to CmsContentAssetImporter.cs before the import code runs so that you are essentially starting from scratch every time.
// Delete old content first if you want. For testing only. var contentRepo = ServiceLocator.Current.GetInstance<IContentRepository>(); var rootFolder = contentRepo.GetChildren<ContentFolder>(SiteDefinition.Current.GlobalAssetsRoot) .Where(f => string.Compare(f.Name, "Catalogs", StringComparison.OrdinalIgnoreCase) == 0) // "Catalogs" is the default name for the media asset folder created for this tool. .FirstOrDefault() as ContentFolder; // Delete blobs var childAssets = contentRepo.GetChildren<MediaData>(rootFolder.ContentLink); var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>(); foreach (MediaData childAsset in childAssets) { var fileBlob = childAsset.BinaryData as FileBlob; if (fileBlob != null) { var fileInfo = new FileInfo(fileBlob.FilePath); File.Delete(fileBlob.FilePath); Directory.Delete(fileInfo.Directory.FullName); } } // Delete content contentRepo.DeleteChildren(rootFolder.ContentLink, true, AccessLevel.NoAccess); //contentRepo.Delete(rootFolder.ContentLink, true, AccessLevel.NoAccess); // Uncomment to delete the media folder created by the tool as well.
That’s just one use case, and you obviously wouldn’t want this to run when you are really ready to import, so please take caution.
TL;DR
- Update the Asset Importer project to match your Commerce site by installing NuGet packages.
- Get the fixes here.
- Run the tool as documented.
If you need to solve this problem, I hope this helps. Please feel free to contact me with any questions/suggestions/bugs in EPiServer Expert Services by emailing me at jooeun.lee@episerver.com or by leaving a comment below.