How to write an Export Plugin

If you want to export data in a particular format, then the export framework can provide you preprocessed data through the  IExportProvider interface. The export provider just defines how to format the data. All the rest is done by the export framework. The merchant selects your provider when he creates an export profile. The export profile is a plan that defines all aspects of an export such as data partitioning, filtering, projection, configuration, deployment etc.

If you are not familiar with developing plugins for SmartStore.NET, please also have look at the tutorial How to write a Plugin.

The IExportProvider interface

The first step in your plugin is to create an export provider class that inherits from ExportProviderBase or directly implements IExportProvider. Decorate the class with the SystemName attribute and optionally with FriendlyName and DisplayOrder. We suggest prefixing the system name with Feeds or Exports

Example
	[SystemName("Exports.SmartStoreOrderCsv")]
	[FriendlyName("SmartStore CSV order export")]
	[IsHidden(true)]
	public class OrderCsvExportProvider : ExportProviderBase

The rarely used IsHidden attribute hides the provider so that it cannot be selected by the merchant (when creating an export profile).

Example
	[SystemName("Feeds.BilligerProductXml")]
	[FriendlyName("Billiger XML product feed")]
	[DisplayOrder(1)]
	[ExportFeatures(Features =
		ExportFeatures.CreatesInitialPublicDeployment |
		ExportFeatures.CanProjectAttributeCombinations)]
	public class ProductXmlExportProvider : ExportProviderBase

The ExportFeatures attribute allows you to control the data processing, the provided data and the projection of an export profile. It is an enumeration with the following bitwise OR-combined values:

  • None: There are no features supported by the provider.
  • CreatesInitialPublicDeployment: Whether to automatically create a file based public deployment when an export profile is created.
  • CanOmitGroupedProducts: Whether to offer option to include/exclude grouped products.
  • CanProjectAttributeCombinations: Whether to offer option to export attribute combinations as products.
  • CanProjectDescription: Whether to offer further options to manipulate the product description.
  • OfferBrandFallback: Whether to offer option to enter a brand fallback.
  • CanIncludeMainPicture: Whether to offer option to set a picture size and to get the URL of the main image.
  • UsesSkuAsMpnFallback: Whether to use SKU as manufacturer part number if MPN is empty.
  • OffersShippingTimeFallback: Whether to offer option to enter a shipping time fallback.
  • OffersShippingCostsFallback: Whether to offer option to enter a shipping costs fallback and a free shipping threshold
  • UsesOldPrice: Whether to get the calculated old product price.
  • UsesSpecialPrice: Whether to get the calculated special and regular (ignoring special offers) price.
  • CanOmitCompletionMail: Whether to automatically send a notification email about the completion of an export task.

The next step is to override the properties and methods of  ExportProviderBase ExportProviderBase implements  IExportProvider for you.

  • EntityType (property): Specifies the entity type to be exported. Current Product, Category, Manufacturer, Customer, Order and NewsLetterSubscription are available. Product is the default.
  • FileExtension (property): Specifies the file extension (without dot) of the export file(s). Return null for a non file based, on-the-fly export.
  • ConfigurationInfo   (property): Specifies information about a partial view that is embedded in the profile edit page containing your provider specific configuration data. Return null when no provider specific configuration is required.
  • Export (method): Called to export data into a file. The IExportExecuteContext parameter provides all required information. More on  this later.
  • OnExecuted (method): Called after the data of one store has been exported.
     
Example: Google Merchant Center product feed
	[SystemName("Feeds.GoogleMerchantCenterProductXml")]
	[FriendlyName("Google Merchant Center XML product feed")]
	[DisplayOrder(1)]
	[ExportFeatures(Features =
		ExportFeatures.CreatesInitialPublicDeployment |
		ExportFeatures.CanOmitGroupedProducts |
		ExportFeatures.CanProjectAttributeCombinations)]
	public class GmcXmlExportProvider : ExportProviderBase
	{
		private const string _googleNamespace = "http://base.google.com/ns/1.0";

		public static string SystemName
		{
			get { return "Feeds.GoogleMerchantCenterProductXml"; }
		}

		public override ExportConfigurationInfo ConfigurationInfo
		{
			get
			{
				return new ExportConfigurationInfo
				{
					PartialViewName = "~/Plugins/SmartStore.GoogleMerchantCenter/Views/FeedGoogleMerchantCenter/ProfileConfiguration.cshtml",
					ModelType = typeof(ProfileConfigurationModel),
					Initialize = obj =>
					{
						var model = (obj as ProfileConfigurationModel);
						model.AvailableGoogleCategories = _googleFeedService.GetTaxonomyList();
					}
				};
			}
		}

		public override string FileExtension
		{
			get { return "xml"; }
		}

		protected override void Export(IExportExecuteContext context)
		{
			dynamic currency = context.Currency;
			var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel();

			using (var writer = XmlWriter.Create(context.DataStream, ExportXmlHelper.DefaultSettings))
			{
				writer.WriteStartDocument();
				writer.WriteStartElement("rss");
				writer.WriteAttributeString("version", "2.0");
				writer.WriteAttributeString("xmlns", "g", null, _googleNamespace);
				writer.WriteStartElement("channel");
				writer.WriteElementString("title", "{0} - Feed for Google Merchant Center".FormatInvariant((string)context.Store.Name));
				writer.WriteElementString("link", "http://base.google.com/base/");
				writer.WriteElementString("description", "Information about products");

				while (context.Abort == ExportAbortion.None && context.Segmenter.ReadNextSegment())
				{
					var segment = context.Segmenter.CurrentSegment;

					int[] productIds = segment.Select(x => (int)((dynamic)x).Id).ToArray();
					var googleProducts = _googleFeedService.GetGoogleProductRecords(productIds);

					foreach (dynamic product in segment)
					{
						if (context.Abort != ExportAbortion.None)
							break;

						Product entity = product.Entity;
						var gmc = googleProducts.FirstOrDefault(x => x.ProductId == entity.Id);

						if (gmc != null && !gmc.Export)
							continue;

						writer.WriteStartElement("item");

						try
						{
							// product item processing skipped here.... the complete code is available on GitHub

							++context.RecordsSucceeded;
						}
						catch (Exception exc)
						{
							context.RecordException(exc, entity.Id);
						}

						writer.WriteEndElement(); // item
					}
				}
				writer.WriteEndElement(); // channel
				writer.WriteEndElement(); // rss
				writer.WriteEndDocument();
			}
		}
	} 

The above example shows the main part of the Google Merchant Center product feed export provider. The item processing statements have been removed for the sake of clarity. You can find them on GitHub.

ConfigurationInfo and ConfigurationData

The ConfigurationInfo property tells the export framework where to find the partial view with the provider specific configuration, the type of the view model and the optional callback called to initialize a view model instance. If your provider does not require any configuration, simply return null for ConfigurationInfo. Partial view and its view model lie in your plugin, but the view is automatically embedded in a tab in the profile edit page. Therefore, the merchant can configure your provider for each profile separately and does not have to leave the profile edit page to do so. In the above example, the view model instance is initialized with a list of all available Google categories. If your view model does not require any initialization, simply return null for Initialize.

ConfigurationData is a property of type object. You can cast it to your view model type to get access to the configuration values stored together with the export profile.

Example
protected override void Export(IExportExecuteContext context)
{
	// ProfileConfigurationModel is your view model containing configuration for your export provider
	var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel();
	//...

Export method and IExportExecuteContext parameter

This is the main routine where all the provider specific data formatting happens. The Export method is called once or multiple times per store, depending on how the merchant configured the partitioning. IExportExecuteContext.DataStream gives you the stream object to be used for writing the export data. Properties and methods of IExportExecuteContext:

  • Segmenter: The data segementer that provides the data to be exported. Typicallly used like in the above sample.
  • Store: Dynamic property which represents the store context for the export.
  • CustomerDynamic property which represents the customer context for the export.
  • CurrencyDynamic property which represents the currency context for the export.
  • LanguageDynamic property which represents the language context for the export.
  • Projection: Projection settings for the export.
  • Log: The file logger instance to be used for any log information.
  • Abort: Property that indicates an abortion of the export. ExportAbortion.None means no abortion, ExportAbortion.Soft means breaking the entity processing but not the rest of the execution (typically used for demo mode limitations) and ExportAbortion.Hard means breaking the processing immediately.
  • DataStreamId: Identifier of current data stream. Can be null.
  • DataStream: Stream used to write data to.
  • ExtraDataStreams: List with extra data streams required by provider.
  • MaxFileNameLength: The maximum length for export file names.
  • Folder: The path of the export content folder.
  • FileName: The name of the current export file.
  • HasPublicDeployment: Whether the profile has a public deployment into the Exchange folder of the store.
  • PublicFolderPath: The local path to the public Exchange folder of the store. It is null if the profile has no public deployment.
  • PublicFileUrl: The public URL of the export file (accessible through the internet). It is  null  if the profile has no public deployment.
  • ConfigurationData: Property of type object with provider specific configuration data.
  • CustomProperties: Use this dictionary for any custom data required along the whole export\profile execution.
  • RecordsSucceeded: Number of successful processed records.
  • RecordsFailed: Number of failed records.
  • RecordException: Method (helper) that processes an exception that occurred while exporting a single record (see above example).

Data provided as dynamic objects

IExportExecuteContext provides entity related data as dynamic objects. The property names of the dynamic object and the entity are always equal. One big advantage of dynamic objects is that they can be extended by compound data. Compound properties of IExportExecuteContext always begin with an underline to avoid conflicts with property names of the entity. For example, the dynamic product in the above sample might look like this during run-time:

Example: Product dynamic object
product._BasePriceInfo = "850,50 € * / 1 kg"
product._DetailUrl = "http://www.my-store.com/bundle-item-2"
product._MainPictureUrl = "http://www.my-store.com/Media/Thumbs/defa/default-image.jpg"
product.Entity = {System.Data.Entity.DynamicProxies.Product_4AB12F306D90498D3CC2F63C571A305BE0C1BFFF82EB479D91FA44B652150F8F}
product.Id = 820
product.Name = "Black Truffle Salt (Tartufo Nero)"
product.Price = 49.5
product.ProductCategories = {System.Collections.Generic.List<object>}
product.LimitedToStores = false
...

_MainPictureUrl, for instance, is not an entity property. It is a computed value attached to the dynamic product object. Some compound properties are only provided if the corresponding ExportFeatures value is set. Other compound properties such as product._DetailUrl are always attached. The property Entity, for instance, gives you access to the underlying original entity object and is attached to each dynamic object.