Creating an Export provider
Export providers allow you to export your store data in many different formats. Smartstore mainly uses CSV and XML. In this tutorial you will write an export provider for the product catalog.
Create a configuration
If you want make your export provider customizable, you will need to add some resources. This step is optional. There are three things to configure:
The
ProfileConfigurationModel
that describes the configurable data.The
HelloWorldConfigurationViewComponent
, which converts stored data into usable formats for your view.The view shown to the user. This acts like a widget view and must adhere to the same directory structure.
This configuration allows you to limit the number of rows that are exported.
The Model
Create the ProfileConfigurationModel.cs
class and add it to the Models directory.
namespace MyOrg.HelloWorld.Models
{
[Serializable, CustomModelPart]
[LocalizedDisplay("Plugins.MyOrg.HelloWorld.")]
public class ProfileConfigurationModel
{
[LocalizedDisplay("*NumberOfExportedRows")]
public int NumberOfExportedRows { get; set; } = 10;
}
}
Add the resources to your localization file.
<LocaleResource Name="NumberOfExportedRows">
<Value>Number of rows</Value>
</LocaleResource>
<LocaleResource Name="NumberOfExportedRows.Hint">
<Value>Number of rows to be exported.</Value>
</LocaleResource>
The ViewComponent
Create the HelloWorldConfigurationViewComponent.cs
class and add it to the Components directory.
namespace MyOrg.HelloWorld.Components
{
public class HelloWorldConfigurationViewComponent : SmartViewComponent
{
public IViewComponentResult Invoke(object data)
{
var model = data as ProfileConfigurationModel;
return View(model);
}
}
}
The View
Create a Razor view Default.cshtml
and add it to Views / Shared / Components / HelloWorldConfiguration.
@model ProfileConfigurationModel
@{
Layout = null;
}
<div class="adminContent">
<div class="adminRow">
<div class="adminTitle">
<smart-label asp-for="NumberOfExportedRows" />
</div>
<div class="adminData">
<input asp-for="NumberOfExportedRows" />
<span asp-validation-for="NumberOfExportedRows"></span>
</div>
</div>
</div>
Add export providers
The CSV export provider
Create the HelloWorldCsvExportProvider.cs
class and add it to the new Providers directory.
namespace MyOrg.HelloWorld.Providers
{
public class HelloWorldCsvExportProvider : ExportProviderBase
{
protected override Task ExportAsync(ExportExecuteContext context, CancellationToken cancelToken)
{
}
}
}
This class inherits from the abstract base class ExportProviderBase
, so you must override the ExportAsync
method.
Add Attributes
Smartstore uses the following attributes to correctly integrate the providers: SystemName
, FriendlyName
and the optional ExportFeatures
.
Add these attributes to your class definition.
[SystemName("MyOrg.HelloWorld.ProductCsv")]
[FriendlyName("Hello world CSV product feed")]
[ExportFeatures(Features =
ExportFeatures.CreatesInitialPublicDeployment |
ExportFeatures.OffersBrandFallback)]
CreatesInitialPublicDeployment
Automatically create a file-based public deployment when an export profile is created.
CanOmitGroupedProducts
Offer option to include/exclude grouped products.
CanProjectAttributeCombinations
Provide the ability to export attribute combinations as products.
CanProjectDescription
Offer more options to manipulate the product description.
OffersBrandFallback
Offer the option to enter a brand fallback.
CanIncludeMainPicture
Provide an option to set an image size and get the URL of the main image.
UsesSkuAsMpnFallback
Use SKU as manufacturer part number if MPN is blank.
OffersShippingTimeFallback
Provide an option to enter a shipping time fallback.
OffersShippingCostsFallback
Offer the option to enter a shipping cost fallback and a free shipping threshold.
CanOmitCompletionMail
Automatically send a completion email.
UsesAttributeCombination
Provide additional data for attribute combinations.
UsesAttributeCombinationParent
Export attribute combinations as products, including the parent product. This is only effective in conjunction with the CanProjectAttributeCombinations export feature.
UsesRelatedDataUnits
Provide additional data units for related data.
To tell the provider that you want to export a CSV file, override the FileExtension
property. The Localizer
is used to localize messages. CsvConfiguration
specifies CSV format details.
public override string FileExtension => "CSV";
public Localizer T { get; set; } = NullLocalizer.Instance;
private CsvConfiguration _csvConfiguration;
private CsvConfiguration CsvConfiguration
{
get
{
_csvConfiguration ??= new CsvConfiguration
{
Delimiter = ';',
SupportsMultiline = false
};
return _csvConfiguration;
}
}
Add configuration
Next, you need to tell the provider how it should be configured (ViewComponent and Model). This is done with the ConfigurationInfo
method.
public override async ExportConfigurationInfo ConfigurationInfo => new()
{
ConfigurationWidget = new ComponentWidget<HelloWorldConfigurationViewComponent>(),
ModelType = typeof(ProfileConfigurationModel)
};
Export data
You are now ready to begin exporting your data. Going back to the ExportAsync
method, start by retrieving the profile configuration data.
var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel();
Get a CsvWriter
and write the first row of column names.
using var writer = new CsvWriter(new StreamWriter(context.DataStream, Encoding.UTF8, 1024, true));
writer.WriteFields(new string[]
{
"ProductName",
"SKU",
"Price",
"Savings",
"Description"
});
writer.NextRow();
Now iterate over the data segments.
while (context.Abort == DataExchangeAbortion.None && await context.DataSegmenter.ReadNextSegmentAsync())
{
var segment = await context.DataSegmenter.GetCurrentSegmentAsync();
}
Inside the while loop, we get the product and it's entity.
foreach (dynamic product in segment)
{
if (context.Abort != DataExchangeAbortion.None)
{
break;
}
Product entity = product.Entity;
}
Add a try-catch block for error handling.
try
{
// Export Product data
}
catch (OutOfMemoryException ex)
{
context.RecordOutOfMemoryException(ex, entity.Id, T);
context.Abort = DataExchangeAbortion.Hard;
throw;
}
catch (Exception ex)
{
context.RecordException(ex, entity.Id);
}
Now calculate the savings within the try block.
var calculatedPrice = (CalculatedPrice)product._Price;
var saving = calculatedPrice.Saving;
Next, we write the fields in the order of our columns. Then we increment the number of rows.
writer.WriteFields(new string[]
{
product.Name,
product.Sku,
((decimal)product.Price).FormatInvariant(),
saving.HasSaving ? saving.SavingPrice.Amount.FormatInvariant() : string.Empty,
((string)product.FullDescription).Truncate(5000)
});
writer.NextRow();
context.RecordsSucceeded++;
Finally, you want to limit your row exports to NumberOfExportedRows
from the profile configuration data.
if (context.RecordsSucceeded >= config.NumberOfExportedRows)
{
context.Abort = DataExchangeAbortion.Soft;
}
Your code could look like this:
namespace MyOrg.HelloWorld.Providers
{
[SystemName("MyOrg.HelloWorld.ProductCsv")]
[FriendlyName("Hello world CSV product feed")]
[ExportFeatures(Features =
ExportFeatures.CreatesInitialPublicDeployment |
ExportFeatures.OffersBrandFallback)]
public class HelloWorldCsvExportProvider : ExportProviderBase
{
public override string FileExtension => "CSV";
public Localizer T { get; set; } = NullLocalizer.Instance;
private CsvConfiguration _csvConfiguration;
private CsvConfiguration CsvConfiguration
{
get
{
_csvConfiguration ??= new CsvConfiguration
{
Delimiter = ';',
SupportsMultiline = false
};
return _csvConfiguration;
}
}
public override ExportConfigurationInfo ConfigurationInfo => new()
{
ConfigurationWidget = new ComponentWidget<HelloWorldConfigurationViewComponent>(),
ModelType = typeof(ProfileConfigurationModel)
};
protected override async Task ExportAsync(ExportExecuteContext context, CancellationToken cancelToken)
{
var config = (context.ConfigurationData as ProfileConfigurationModel) ?? new ProfileConfigurationModel();
using var writer = new CsvWriter(new StreamWriter(context.DataStream, Encoding.UTF8, 1024, true));
writer.WriteFields(new string[]
{
"ProductName",
"SKU",
"Price",
"Savings",
"Description"
});
writer.NextRow();
while (context.Abort == DataExchangeAbortion.None && await context.DataSegmenter.ReadNextSegmentAsync())
{
var segment = await context.DataSegmenter.GetCurrentSegmentAsync();
foreach (dynamic product in segment)
{
if (context.Abort != DataExchangeAbortion.None)
{
break;
}
Product entity = product.Entity;
try
{
var calculatedPrice = (CalculatedPrice)product._Price;
var saving = calculatedPrice.Saving;
writer.WriteFields(new string[]
{
product.Name,
product.Sku,
((decimal)product.Price).FormatInvariant(),
saving.HasSaving ? saving.SavingPrice.Amount.FormatInvariant() : string.Empty,
((string)product.FullDescription).Truncate(5000)
});
writer.NextRow();
++context.RecordsSucceeded;
if (context.RecordsSucceeded >= config.NumberOfExportedRows)
{
context.Abort = DataExchangeAbortion.Soft;
}
}
catch (OutOfMemoryException ex)
{
context.RecordOutOfMemoryException(ex, entity.Id, T);
context.Abort = DataExchangeAbortion.Hard;
throw;
}
catch (Exception ex)
{
context.RecordException(ex, entity.Id);
}
}
}
}
}
}
The XML export provider
XML export is very similar to CSV export. First, you need to change the file extension to XML
.
public override string FileExtension => "XML";
The Localizer
and ConfigurationInfo
stay the same. Now you just need to change ExportAsync
. For XML, you get the writer from an ExportXmlHelper
.
using var helper = new ExportXmlHelper(context.DataStream);
var writer = helper.Writer;
Next, you start the document and write the grouping tag.
writer.WriteStartDocument();
writer.WriteStartElement("products");
As with the CSV provider, you fetch the next data segment, iterating through the products.
while (context.Abort == DataExchangeAbortion.None && await context.DataSegmenter.ReadNextSegmentAsync())
{
var segment = await context.DataSegmenter.GetCurrentSegmentAsync();
foreach (dynamic product in segment)
{
if (context.Abort != DataExchangeAbortion.None)
{
break;
}
Product entity = product.Entity;
}
}
Write a new Product XML node with the values you want to export.
writer.WriteStartElement("product");
try
{
var calculatedPrice = (CalculatedPrice)product._Price;
var saving = calculatedPrice.Saving;
writer.WriteElementString("product-name", (string)product.Name);
writer.WriteElementString("sku", (string)product.Sku);
writer.WriteElementString("price", ((decimal)product.Price).FormatInvariant());
if (saving.HasSaving)
{
writer.WriteElementString("savings", saving.SavingPrice.Amount.FormatInvariant());
}
writer.WriteCData("desc", ((string)product.FullDescription).Truncate(5000));
context.RecordsSucceeded++;
// Row limitation and catch block left out for brevity
}
writer.WriteEndElement(); // product
And when the while loop is complete, the grouping and the document must be closed.
writer.WriteEndElement(); // products
writer.WriteEndDocument();
The source code can be found in HelloWorldXmlExportProvider.cs
.
Delete export profiles
Finally, you just need to clean up any existing profiles on UnistallAsync
in Module.cs
. Pass SmartDbContext
and IExportProfileService
to the module class constructor.
private readonly SmartDbContext _db;
private readonly IExportProfileService _exportProfileService;
public Module(SmartDbContext db, IExportProfileService exportProfileService)
{
_db = db;
_exportProfileService = exportProfileService;
}
Then add the following lines to the top of your UninstallAsync
method
// Read the export profile entities associated with your export provider
var profiles = await _db.ExportProfiles
.Include(x => x.Deployments)
.Include(x => x.Task)
.Where(x => x.ProviderSystemName == "MyOrg.HelloWorld.ProductCsv" || x.ProviderSystemName == "MyOrg.HelloWorld.ProductXml")
.ToListAsync();
// Now delete the entities and any related file
await profiles.EachAsync(x => _exportProfileService.DeleteExportProfileAsync(x, true));
Conclusion
In this tutorial, you created an export provider. You have created a configuration profile and a CSV export provider.
Last updated
Was this helpful?