Piping data to an application in .NET Core
Earlier today I had a need for a PlantUML API that could generate PlantUML diagrams without having to install the necessary dependencies on my PC. To achieve this I decided to write a small API using ASP.NET Core.
I was thinking of just calling the PlantUML binary with the input data from a HTTP POST body. There could be some security implications but I was the only user of the system so it didn’t really matter. I was thinking piping the input data should be a very simple thing to do. It sure was in the end but there were a few things that I forgot about when dealing with streams.
I implemented an API for it and started writing code to do the piping.
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _appSettings.JavaExecutablePath,
Arguments = $"-jar {_appSettings.PlantUMLPath} -pipe"
RedirectStandardInput = true,
RedirectStandardOutput = true,
}
};
process.Start();
await userInputtedPlantUMLStream.CopyToAsync(process.StandardInput.BaseStream);
var outputStream = new MemoryStream();
await process.StandardOutput.BaseStream.CopyToAsync(outputStream);
process.WaitForExit();
outputStream.Seek(0, SeekOrigin.Begin);
return outputStream;
I thought this should do the trick. I started the API and did a request but the application would just hang. After quick debugging I noticed the application was hanging at:
await process.StandardOutput.BaseStream.CopyToAsync(outputStream);
Took me a while to figure out what was going on. I was trying to look at examples of reading standard output stream in .NET Core documentation and Stack Overflow. I looked at the examples and it seemed all them did the same things as I did. Turns out I missed one key thing. None of the examples were piping in the data… There was one crucial thing I forgot to do: I should close the standard input once I’m done piping the data in. The fix was really simple after all
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = _appSettings.JavaExecutablePath,
Arguments = $"-jar {_appSettings.PlantUMLPath} -pipe"
RedirectStandardInput = true,
RedirectStandardOutput = true,
}
};
process.Start();
await userInputtedPlantUMLStream.CopyToAsync(process.StandardInput.BaseStream);
// One should not forget to close the stdin.. :)
process.StandardInput.Close();
var outputStream = new MemoryStream();
await process.StandardOutput.BaseStream.CopyToAsync(outputStream);
process.WaitForExit();
outputStream.Seek(0, SeekOrigin.Begin);
return outputStream;
One missing line can make a huge difference.