Skip to content

Instantly share code, notes, and snippets.

@mortenholmgaard
Last active December 1, 2017 06:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mortenholmgaard/57fbe361ebe079bd1c97caf74de6a0cf to your computer and use it in GitHub Desktop.
Save mortenholmgaard/57fbe361ebe079bd1c97caf74de6a0cf to your computer and use it in GitHub Desktop.
Azure blob image provider for Episerver commerce product images
private readonly ProductImageContentProvider _productImageContentProvider;
private void AddImageInEpiServerBackend(string colorProductCode, string imageName)
{
var colorProductLink = _referenceConverterFactory.GetReferenceConverter().GetContentLinks(new []{ colorProductCode }).Values.FirstOrDefault();
ColorProduct colorProduct = _contentRepository.Get<ColorProduct>(colorProductLink).CreateWritableClone() as ColorProduct;
ProductImageFile productImageFile = _productImageContentProvider.ConvertToProductImageFile(imageName);
colorProduct.CommerceMediaCollection.Add(new CommerceMedia
{
AssetLink = productImageFile.ContentLink,
AssetType = "episerver.core.icontentimage",
GroupName = "default",
SortOrder = 0
});
_contentRepository.Save(colorProduct, SaveAction.Publish, AccessLevel.NoAccess);
}
The problem with the setting productImageFile.LargeThumbnail to a AzureBlob is that ValidateIdentifier() method in Blob is static and requires scheme to be "epi.fx.blob"
if (id.Scheme != "epi.fx.blob")
throw new ArgumentException("Scheme of a blob identifier must be epi.fx.blob", "id");
Because it is static it is not possible to override it and it is called from EPiServer.SpecializedProperties.PropertyBlob so I can't override that either.
EPiServer.Core.InvalidPropertyValueException: "https://pompdeluxdevelopment.blob.core.windows.net/pompdelux/images/AW17/AikenYoSoftshellJacketAW17_DarkPurple_overview_01.png" is not a valid value for "LargeThumbnail". ---> System.ArgumentException: Scheme of a blob identifier must be epi.fx.blob
Parameter name: id
at EPiServer.Framework.Blobs.Blob.ValidateIdentifier(Uri id, Nullable`1 testForFile)
at EPiServer.SpecializedProperties.PropertyBlob.<>c__DisplayClass10_0.<set_Value>b__0()
at EPiServer.Core.PropertyData.SetPropertyValue(Object value, SetPropertyValueDelegate doSet)
--- End of inner exception stack trace ---
at EPiServer.Core.PropertyData.ThrowEditorFriendlyException(Object value, Exception e)
at EPiServer.Core.PropertyData.SetPropertyValue(Object value, SetPropertyValueDelegate doSet)
at EPiServer.DataAbstraction.RuntimeModel.ContentDataInterceptor.Intercept(IInvocation invocation)
at Castle.DynamicProxy.AbstractInvocation.Proceed()
at Vertica.Pompdelux.Business.Infrastructure.EPiServer.ProductImageContentProvider.ConvertToProductImageFile(String imageName) in C:\Code\POMPdeLUX\Development\src\Business\Infrastructure\EPiServer\ProductImageContentProvider.cs:line 101
at Vertica.Pompdelux.Website.Jobs.ImportProductImagesJob.AddImageInEpiServerBackend(String colorProductCode, String imageName) in C:\Code\POMPdeLUX\Development\src\Website\Jobs\ImportProductImagesJob.cs:line 337
at Vertica.Pompdelux.Website.Jobs.ImportProductImagesJob.Execute() in C:\Code\POMPdeLUX\Development\src\Website\Jobs\ImportProductImagesJob.cs:line 170
And the Component does not work either - when accessing episerver/cms the main area goes all white.
I get this error in the EPiServerErrorWarn.log: WARN EPiServer.Shell.Navigation.MenuAssembler: Could not find the parent path [/global/find]
The code is based on this guide: http://world.episerver.com/blogs/Per-Magne-Skuseth/Dates/2014/11/content-providers-101--part-i-introduction-initialization-ui--identity-mapping/
namespace Vertica.Pompdelux.Website.Infrastructure.Boostrapping
{
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
public class InitializationModule : IConfigurableModule
{
public void Initialize(InitializationEngine context)
{
...
ConfigureProductImagesProvider(context);
}
private static void ConfigureProductImagesProvider(InitializationEngine context)
{
var providerValues = new NameValueCollection
{
{
ContentProviderElement.EntryPointString, ProductImageContentProvider.GetEntryPoint(ProductImageContentProvider.Key).ContentLink.ToString()
}
};
var productImageContentProvider = context.Locate.Advanced.GetInstance<ProductImageContentProvider>();
productImageContentProvider.Initialize(ProductImageContentProvider.Key, providerValues);
var providerManager = context.Locate.Advanced.GetInstance<IContentProviderManager>();
providerManager.ProviderMap.AddProvider(productImageContentProvider);
}
}
}
using EPiServer.Shell;
using EPiServer.Shell.ViewComposition;
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer
{
[Component]
public class ProductImageComponent : ComponentDefinitionBase
{
public ProductImageComponent() : base("epi-cms.component.Media")
{
Categories = new[] { "content" };
Title = "Product images";
Description = "All product images from azure blob storage";
SortOrder = 1000;
PlugInAreas = new[] { PlugInArea.AssetsDefaultGroup, "/episerver/commerce/assets/defaultgroup" };
Settings.Add(new Setting("repositoryKey", ProductImageContentProvider.Key));
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using EPiServer;
using EPiServer.Azure.Blobs;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using Vertica.Pompdelux.Business.Infrastructure.Blob;
using Vertica.Pompdelux.Shared.Cache;
using Vertica.Pompdelux.Shared.Models.Media;
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer
{
/// <summary>
/// https://world.episerver.com/blogs/Per-Magne-Skuseth/Dates/2014/11/content-providers-101--part-i-introduction-initialization-ui--identity-mapping/
/// http://world.episerver.com/forum/developer-forum/EPiServer-Commerce/Thread-Container/2014/9/Uploading-CommerceMedia-in-assets-programatically/?pageIndex=1
/// </summary>
public class ProductImageContentProvider : ContentProvider
{
private readonly IContentRepository _contentRepository;
private readonly CustomAzureBlobProvider _produtImagesAzureBlobProvider;
private readonly IBlobService _blobService;
private readonly ICacheProvider _cacheProvider;
private const string AllProductImageUrlsCacheKey = "AllProductImageUrls";
public ProductImageContentProvider(IContentRepository contentRepository, IBlobService blobService, ICacheProvider cacheProvider)
{
_contentRepository = contentRepository;
_blobService = blobService;
_cacheProvider = cacheProvider;
_produtImagesAzureBlobProvider = new CustomAzureBlobProvider();
_produtImagesAzureBlobProvider.Initialize("productsinazureblobs", new NameValueCollection { { "connectionStringName", "EPiServerAzureBlobs" }, { "container", "pompdelux" } });
}
public const string Key = "productimages";
protected Injected<IdentityMappingService> IdentityMappingService { get; set; }
protected override IContent LoadContent(ContentReference contentLink, ILanguageSelector languageSelector)
{
MappedIdentity mappedIdentity = IdentityMappingService.Service.Get(contentLink);
// The imageName is found in the ExternalIdentifier that was created earlier. Note that Segments[1] is used due to the fact that the ExternalIdentifier is of type Uri.
// It contains two segments. Segments[0] contains the content provider key, and Segments[1] contains the unique path, which is the imageName in this case.
string imageName = mappedIdentity.ExternalIdentifier.Segments[1];
var productImageFile = ConvertToProductImageFile(imageName);
//AddContentToCache(productImageFile); //TODO properly add
return productImageFile;
}
/// <summary>
/// NOTE: productImageUrls is cached in one day
/// This will pass back content reference for all the children for a specific node. In this case, it will be a flat structure,
/// so this will only be loaded with the provider's EntryPoint set as the contentLink
/// </summary>
protected override IList<GetChildrenReferenceResult> LoadChildrenReferencesAndTypes(
ContentReference contentLink, string languageID, out bool languageSpecific)
{
languageSpecific = false;
string[] productImageUrls;
if (!_cacheProvider.TryGet(AllProductImageUrlsCacheKey, out productImageUrls))
{
productImageUrls = _blobService.GetAllProductImageUrls();
_cacheProvider.Put(AllProductImageUrlsCacheKey, productImageUrls, CacheProvider.OneDay);
}
// create and return GetChildrenReferenceResults. The ContentReference (ContentLink) is fetched using the IdentityMapingService.
return productImageUrls.Select(x =>
new GetChildrenReferenceResult
{
ContentLink = IdentityMappingService.Service.Get(MappedIdentity.ConstructExternalIdentifier(ProviderKey, x)).ContentLink,
ModelType = typeof(ProductImageFile)
}).ToList();
}
protected override Uri ConstructContentUri(int contentTypeId, ContentReference contentLink, Guid contentGuid)
{
return base.ConstructContentUri(contentTypeId, contentLink, contentGuid);
}
protected override IList<MatchingSegmentResult> ListMatchingSegments(ContentReference parentLink, string urlSegment)
{
//return base.ListMatchingSegments(parentLink, urlSegment);
var list = new List<MatchingSegmentResult>();
foreach (var child in LoadChildren<IContent>(parentLink, LanguageSelector.Fallback("da-DK", true), -1, -1))
{
var routable = child as IRoutable;
var isMatch = routable != null && urlSegment.Equals(routable.RouteSegment, StringComparison.OrdinalIgnoreCase);
if (isMatch)
{
list.Add(new MatchingSegmentResult
{
ContentLink = child.ContentLink
});
}
}
return list;
}
public ProductImageFile ConvertToProductImageFile(string imageName)
{
ProductImageFile productImageFile = _contentRepository.GetDefault<ProductImageFile>(DataFactory.Instance.Get<ContentFolder>(EntryPoint).ContentLink);
productImageFile.Status = VersionStatus.Published;
productImageFile.IsPendingPublish = false;
productImageFile.StartPublish = DateTime.Now.Subtract(TimeSpan.FromDays(14));
// This part is a bit tricky. IdentityMappingService is used in order to create the ContentReference and content GUID. Creates an external identifier based on the imageName
Uri externalId = MappedIdentity.ConstructExternalIdentifier(ProviderKey, imageName);
// Make sure Get is invoked with the second parameter ('createMissingMapping') set to true. This will create a new mapping if no existing mapping is found
MappedIdentity mappedContent = IdentityMappingService.Service.Get(externalId, true);
productImageFile.ContentLink = mappedContent.ContentLink;
productImageFile.ContentGuid = mappedContent.ContentGuid;
var blobPath = $"epi.fx.blob://productimages/images/{GetSeason(imageName)}/{imageName}";
var blob = _produtImagesAzureBlobProvider.GetBlob(new Uri(MakeValidBlobPathForEpiServerValidation(blobPath)));
productImageFile.Name = imageName;
productImageFile.BinaryData = blob;
productImageFile.Thumbnail = blob;
productImageFile.LargeThumbnail = blob;
productImageFile.MakeReadOnly();
return productImageFile;
}
public static ContentFolder GetEntryPoint(string name)
{
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
var folder = contentRepository.GetBySegment(ContentReference.RootPage, name, LanguageSelector.AutoDetect()) as ContentFolder;
if (folder == null)
{
folder = contentRepository.GetDefault<ContentFolder>(ContentReference.RootPage);
folder.Name = name;
contentRepository.Save(folder, SaveAction.Publish, AccessLevel.NoAccess);
}
return folder;
}
/// <summary>
/// Make a url to circumvent Blob method: ValidateIdentifier(Uri id, bool? testForFile)
/// Is changed back in CustomAzureBlobContainer
/// E.g. "epi.fx.blob://productimages/images/AW17/imageName.png" ==> "epi.fx.blob://productimages/images/AW17|imageName.png"
/// </summary>
/// <returns>Valid Episerver blob url</returns>
private string MakeValidBlobPathForEpiServerValidation(string url)
{
if (url.StartsWith("epi.fx.blob://default"))
return url;
const int numberOfSlashes = 4;
var builder = new StringBuilder();
var urlParts = url.Split('/');
for (var i = 0; i < urlParts.Length; i++)
{
if (i > 0)
builder.Append(i > numberOfSlashes ? "|" : "/");
builder.Append(urlParts[i]);
}
url = builder.ToString();
return url;
}
public static string GetSeason(string imageName)
{
var styleCode = imageName.Split('_').First();
return styleCode.Substring(styleCode.Length - 4, 4);
}
}
}
using System.ComponentModel.DataAnnotations;
using EPiServer.Commerce.SpecializedProperties;
using EPiServer.DataAnnotations;
namespace Vertica.Pompdelux.Shared.Models.Media
{
[AdministrationSettings(
CodeOnly = true,
GroupName = "ProductImages")]
[ContentType(GUID = "5CF67A99-CB6D-4DFB-9EB7-7692B5F14187",
DisplayName = "Product image",
GroupName = "ProductImages")]
public class ProductImageFile : CommerceImage
{
}
}
using System;
using System.Collections.Generic;
using EPiServer.Core;
using EPiServer.ServiceLocation;
using EPiServer.Shell;
using Vertica.Pompdelux.Shared.Models.Media;
namespace Vertica.Pompdelux.Business.Infrastructure.EPiServer
{
[ServiceConfiguration(typeof(IContentRepositoryDescriptor))]
public class ProductImageRepositoryDescriptor : ContentRepositoryDescriptorBase
{
protected Injected<IContentProviderManager> ContentProviderManager { get; set; }
public override string Key => ProductImageContentProvider.Key;
public override string Name => "Product images";
public override IEnumerable<ContentReference> Roots => new[] { ContentProviderManager.Service.GetProvider(ProductImageContentProvider.Key).EntryPoint };
public override IEnumerable<Type> ContainedTypes => new[] { typeof(ProductImageFile) };
public override IEnumerable<Type> MainNavigationTypes => new[] { typeof(ContentFolder) };
public override IEnumerable<Type> CreatableTypes => new[] { typeof(ProductImageFile) };
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment