How to Import Assets into Sitecore Content Hub Programmatically
This blog post describes how to create a Content Hub asset importer using the StyleLabs SDK. This importer can be used in mass asset imports from a variety of data sources.
TL;DR Take a look at the Content Hub Importer solution in GitHub. This solution focuses on the downstream import of assets during initial migrations, or periodic updates; in other words it provides a turn-key solution to integrating with systems on the left side of the Figure 1. Feel free to extend the solution and contribute by submitting pull requests.
Content Hub API
Sitecore Content Hub provides a robust Hypermedia REST API. This API makes exploring Content Hub data intuitive. The easiest way to integrate with the REST API is to use the StyleLabs SDK. The API supports CRUD operations on any entity in the system, which means that we can manipulate not only assets but also access and update other data stored in the hub, for instance, SSO settings; however, that’s for another post. Without further due, let’s jump right in.
Setting Up the Content Hub Importer Solution
- Create a new C# .NET Core project
- Add the Stylelabs.M.Sdk.WebClient NugGet package (https://docs-partners.stylelabs.com/content/integrations/web-sdk/getting-started.html)
- Note: this SDK NuGet source is not public; you need to have a valid partner account to login to MyGet (check with your internal Sitecore partnership management or reach out to your assigned Sitecore Partner Alliance manager to get the login)
Creating Content Hub Asset Importer
- Create a connector instance that will hold the connection singleton
using Stylelabs.M.Sdk.WebClient; using System; namespace ContentHub.Importer.Utils { public static class MConnector { private static Lazy _client { get; set; } public static IWebMClient Client { get { if (_client == null) { var auth = new Stylelabs.M.Sdk.WebClient.Authentication.OAuthPasswordGrant() { ClientId = AppSettings.ClientId, ClientSecret = AppSettings.ClientSecret, UserName = AppSettings.Username, Password = AppSettings.Password }; _client = new Lazy(() => MClientFactory.CreateMClient(AppSettings.Host, auth)); IWebMClient client = MClientFactory.CreateMClient(AppSettings.Host, auth); client.TestConnectionAsync().Wait(); } return _client.Value; } } } }
- Create the AppSettings configuration singleton.
using Microsoft.Extensions.Configuration; using System; using System.IO; namespace ContentHub.Importer.Utils { public static class AppSettings { private static IConfiguration _config; public static IConfiguration Configuration { get { if (_config == null) { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json"); _config = builder.Build(); } return _config; } } public static Uri Host { get { return new Uri($"{Configuration["M:Host"]}"); } } public static string ClientId { get { return $"{Configuration["M:ClientId"]}"; } } public static string ClientSecret { get { return $"{Configuration["M:ClientSecret"]}"; } } public static string Username { get { return $"{Configuration["M:Username"]}"; } } public static string Password { get { return $"{Configuration["M:Password"]}"; } } public static string TempDirectory { get { return $"{Configuration["TempDirectory"]}"; } } } }
And the supporting appsettings.json with the setting values –
{ "M": { "Host": "https://sandboxdomain.stylelabsdemo.com", "ClientId": "MyId", "ClientSecret": "bd35a6e2-bf59-46bb-9475-e5b4aaca8426", "username": "MyUser", "Password": "ContentHubR0ck$!" }, }
To get the ClientId and ClientSecret settings, create a new client under Manage > OAuth clients.
- Add the Asset class (model with operations)
using ContentHub.Importer.Utils; using Stylelabs.M.Base.Querying; using Stylelabs.M.Base.Querying.Linq; using Stylelabs.M.Framework.Essentials.LoadOptions; using Stylelabs.M.Sdk; using Stylelabs.M.Sdk.Contracts.Base; using System; using System.IO; using System.Linq; using System.Threading.Tasks; namespace ContentHub.Importer { public class Asset { public string OriginUrl { get; set; } public string Description { get; set; } public string MarketingDescription { get; set; } public string AssetType { get; set; } public string SocialMediaChannel { get; set; } public string ContentSecurity { get; set; } public string AssetSource { get; set; } public string LifecycleStatus { get; set; } private static string username = "ApiAdmin"; public string Title { get { var lastPart = Path.GetFileNameWithoutExtension(OriginUrl); return lastPart ?? "Fallback Title - CHANGE ME"; } } public static async Task CreateAssetTypeAsync(string name) { // Check if the asset type already exists var query = Query.CreateIdsQuery(entities => from e in entities where e.Property(Constants.AssetType.Properties.Label, Constants.DefaultCulture) == name select e); var result = await MConnector.Client.Querying.QueryIdsAsync(query).ConfigureAwait(false); if (result.Items.Count > 0) return result.Items.First(); // Create a new asset type entity var assetType = await MConnector.Client.EntityFactory.CreateAsync(Constants.AssetType.DefinitionName, CultureLoadOption.Default).ConfigureAwait(false); // Set a human readable identifier assetType.Identifier = $"{Constants.AssetType.DefinitionName}.{name}"; // Set the classification name assetType.SetPropertyValue(Constants.AssetType.Properties.Label, Constants.DefaultCulture, name); // Mark the asset type as a root taxonomy item assetType.IsRootTaxonomyItem = true; // Save the asset type var assetTypeId = await MConnector.Client.Entities.SaveAsync(assetType).ConfigureAwait(false); return assetTypeId; } public static async Task SetupAssetAsync(Asset importedAsset) { // Optional: Impersonate the user to setup the asset var impersonatedClient = await MConnector.Client.ImpersonateAsync(username).ConfigureAwait(false); // Get or create the asset type var assetTypeId = await CreateAssetTypeAsync(importedAsset.AssetType).ConfigureAwait(false); // Create an asset var assetId = await CreateAsset(impersonatedClient, importedAsset, assetTypeId).ConfigureAwait(false); // Create a fetchjob to attach a resource/file to the asset var fetchJobId = await Jobs.CreateFetchJob(assetId, new Uri(importedAsset.OriginUrl)).ConfigureAwait(false); Console.WriteLine($"Added asset {assetId}. Job ID {fetchJobId}"); } public static async Task CreateAsset(IMClient client, Asset importedAsset, long? assetTypeId = null) { // Create the entity resource var asset = await client.EntityFactory.CreateAsync(Constants.Asset.DefinitionName, CultureLoadOption.Default).ConfigureAwait(false); // Set the mandatory title property asset.SetPropertyValue(Constants.Asset.Properties.Title, importedAsset.Title); asset.SetPropertyValue(Constants.Asset.Properties.Description, Constants.DefaultCulture ,importedAsset.Description); asset.SetPropertyValue(Constants.Asset.Properties.AssetSource, importedAsset.AssetSource); asset.SetPropertyValue(Constants.Asset.Properties.MarketingDescription, importedAsset.MarketingDescription); // Link the asset to content repository: standard var standardContentRepository = await client.Entities.GetAsync(Constants.ContentRepositories.Standard).ConfigureAwait(false); var contentRepositoryRelation = asset.GetRelation(Constants.Asset.Relations.ContentRepositoryToAsset); contentRepositoryRelation.Parents.Add(standardContentRepository.Id.Value); // Link the asset to lifecycle var finalLifeCycleCreated = await client.Entities.GetAsync($"{Constants.Asset.LifeCyclePrefix}{importedAsset.LifecycleStatus}").ConfigureAwait(false); var finalLifeCycleRelation = asset.GetRelation(Constants.Asset.Relations.FinalLifeCycleStatusToAsset); finalLifeCycleRelation.Parent = finalLifeCycleCreated.Id.Value; // Link the asset to content security if (!string.IsNullOrWhiteSpace(importedAsset.ContentSecurity)) { var contentSecurityCreated = await client.Entities.GetAsync($"{Constants.Asset.ContentSecurityPrefix}{UppercaseFirst(importedAsset.ContentSecurity).Replace(" ", string.Empty)}").ConfigureAwait(false); var contentSecurityRelation = asset.GetRelation(Constants.Asset.Relations.ContentSecurityToAsset); contentSecurityRelation.Parent = contentSecurityCreated.Id.Value; } // Link the asset to social media if (!string.IsNullOrWhiteSpace(importedAsset.SocialMediaChannel)) { var socialMediaChannelCreated = await client.Entities.GetAsync($"{Constants.Asset.SocialMediaChannelPrefix}{importedAsset.SocialMediaChannel}").ConfigureAwait(false); var socialMediaChannelRelation = asset.GetRelation(Constants.Asset.Relations.SocialMediaChannelToAsset); socialMediaChannelRelation.Parent = socialMediaChannelCreated.Id.Value; } // Link the asset to asset source //var assetSourceCreated = await client.Entities.GetAsync($"AssetSource.Legacy").ConfigureAwait(false); //var assetSourceRelation = asset.GetRelation("AssetSource"); //assetSourceRelation.Parent = assetSourceCreated.Id.Value; // Link the asset to the asset type when specified if (assetTypeId.HasValue) { var assetTypeRelation = asset.GetRelation(Constants.Asset.Relations.AssetTypeToAsset); assetTypeRelation.Parent = assetTypeId.Value; } // Create the asset var assetId = await client.Entities.SaveAsync(asset).ConfigureAwait(false); // Return a reference to the newly created asset return assetId; } static string UppercaseFirst(string s) { // Check for empty string. if (string.IsNullOrEmpty(s)) { return string.Empty; } // Return char and concat substring. return char.ToUpper(s[0]) + s.Substring(1); } } }
- Add the supporting Constants file
using System.Globalization; namespace ContentHub.Importer { public static class Constants { public static readonly CultureInfo DefaultCulture = CultureInfo.GetCultureInfo("en-US"); public static class Job { public const string DefinitionName = "M.Job"; public static class Properties { public const string Condition = "Job.Condition"; public const string State = "Job.State"; public const string Type = "Job.Type"; } public static class Conditions { public const string Failed = "Failed"; public const string Pending = "Pending"; public const string Success = "Success"; } public static class States { public const string Failed = "Failed"; public const string Pending = "Pending"; public const string Completed = "Completed"; } public static class Types { public const string Processing = "Processing"; } } public static class AssetType { public const string DefinitionName = "M.AssetType"; public static class Properties { public const string Label = "Label"; } public static class Relations { public const string AssetTypeToAsset = "AssetTypeToAsset"; } } public static class Asset { public const string DefinitionName = "M.Asset"; public const string AsssetTypeIdPrefix = "M.AssetType."; public const string LifeCyclePrefix = "M.Final.LifeCycle.Status."; public const string ContentSecurityPrefix = "ContentSecurity."; public const string SocialMediaChannelPrefix = "SocialMedia."; public static class MemberGroups { public const string Content = "Content"; } public static class Properties { public const string ApprovalDate = "ApprovalDate"; public const string Title = "Title"; public const string Description = "Description"; public const string MarketingDescription = "MarketingDescription"; public const string FileName = "FileName"; public const string AssetSource = "AssetSource"; } public static class Relations { public const string AssetTypeToAsset = "AssetTypeToAsset"; public const string AssetMediaToAsset = "AssetMediaToAsset"; public const string ContentRepositoryToAsset = "ContentRepositoryToAsset"; public const string FinalLifeCycleStatusToAsset = "FinalLifeCycleStatusToAsset"; public const string ContentSecurityToAsset = "ContentSecurity"; public const string AssetSourceToAsset = "AssetSource"; public const string SocialMediaChannelToAsset = "SocialMediaChannel"; } } public static class ContentRepositories { public const string Standard = "M.Content.Repository.Standard"; } public static class LifeCycleStatus { public const string Created = "M.Final.LifeCycle.Status.Created"; public const string Approved = "M.Final.LifeCycle.Status.Approved"; public const string Rejected = "M.Final.LifeCycle.Status.Rejected"; public const string Archived = "M.Final.LifeCycle.Status.Archived"; public const string RequiresApproval = "M.Final.LifeCycle.Status.RequiresApproval"; } } }
- Call the Asset Importer from your code
await Asset.SetupAssetAsync(asset);
As you can see, importing assets into Sitecore Content Hub is a breeze. There is one thing to be careful about – the LifeCycle status. The Create space in Content Hub is restricted to individual user accounts, while Review and Content spaces can be shared. If you would like to import assets into the Create space, make sure to import them impersonating the right user, which is set in the Asset class; otherwise, the assets will not appear in the Create space.
Quick Tip:
The importer returns the newly created asset ID that can be used to access the asset directly by navigating to https://[content hub domain]/en-us/asset/[asset ID] to view the detail page, or https://[content hub domain]/api/entities/[asset ID] to view the JSON output of the metadata, which also contains a “created_by” property, in case you need to check who the creator was for troubleshooting purposes.