As part of the automated transfer of the production database to the testing environment for my current client we found the need to maintain a node in the Episerver page tree that is used for testing. I extended our ApiController with an export endpoint for this purpose.
Note that this code is never intended to be deployed to production.
[HttpGet]
[Route("v1/export-node")]
public IHttpActionResult ExportTestNode()
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new PushStreamContent((stream, content, context) => _importExportService.ExportTestNode(stream), new MediaTypeHeaderValue("application/zip"))
};
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = "ExportedFile.episerverdata",
};
return new ResponseMessageResult(response);
// Error handling removed to reduce noise.
}
So the interesting part happens in the onStreamAvailable Action in the PushStreamContent constructor. In the ExportTestNode method below, the outputStream is the one provided through the Action. This stream is Write only.
The Episerver export functionality on the other hand requires a stream that is both readable as well as writable and seekable (i.e. a FileStream or a MemoryStream). This forces us to use a second stream before passing the data to the one used in the response.
Should you pass a Write only stream to the Episerver export method, you will get a null reference exception in an internal clean up method (which is not really the true culprit, but rather a symptom). In this example we use a MemoryStream, which may of course cause problems should the export be extremly large.
public class ImportExportService
{
private readonly ILogger _logger = LogManager.GetLogger(typeof(ImportExportService));
internal void ExportTestNode(Stream outputStream)
{
using (outputStream)
{
using (var memoryStream = new MemoryStream())
{
_ = ExportInternal(memoryStream);
memoryStream.Position = 0;
memoryStream.CopyTo(outputStream);
}
}
}
We use the outputStream via an using statement as we require it to be properly closed once we are done writing to it. Should we fail to do this, we will only get the response headers back from the endpoint, and it would be waiting for the body that never comes.
In the private export method you will need a way of getting your hands on a reference to the test node root. This could be as simple as keeping a property set on a page type.
Remember to set the AutoCloseStream property of the ExportOptions to false, as we will need the stream to remain open to be able to work with it. The returned transfer log contains collections with errors and warnings.
private ITransferLog ExportInternal(Stream exportStream)
{
ContentReference root = // Get the test node root for your current website.
ExportSource[] sourceRoots = new[] { new ExportSource(root) };
ExportOptions options = ExportOptions.DefaultOptions;
options.AutoCloseStream = false;
using (IDataExporter exporter = ServiceLocator.Current.GetInstance<IDataExporter>())
{
return exporter.Export(exportStream, sourceRoots, options);
}
}
}
Using the ServiceLocator to get hold of an IDataExporter instance will allow the ImportExportService to be registered as a singleton without rendering it useless due to the exporter being disposed. Could also be done via constructor injection but would require implementing IDisposable.