How to consume a file with a ServiceStack client

2019-03-09 22:52发布

问题:

I am trying to use ServiceStack to return a file to a ServiceStack client in a RESTful manner.

I have read other questions on SO (here and here) which advise using HttpResult and a FileInfo object or MemoryStream to allow the ContentType header to be changed to the relevant file type.

This works for me when I call the service via a browser, the correct file automatically starts to download. How do I consume the file using one of the ServiceStack clients though?

I'm using a Request DTO and trying to return using something similar to

return new HttpResult(new FileInfo("file.xml"), asAttachment:true) {
   ContentType = "text/xml"
};

How would I consume this with the JsonServiceClient for example?

回答1:

You wouldn't consume files with the ServiceStack's .NET ServiceClients as they're mainly for sending DTO's.

You can just use any normal WebRequest to download files, in the v3.9.33 of ServiceStack introduced some handy WebRequest extensions HTTP Utils that make this easy, e.g:

For a text file:

var xmlFile = downloadUrl.GetXmlFromUrl(responseFilter: httpRes => {
        var fileInfoHeaders = httpRes.Headers[HttpHeaders.ContentDisposition];
    });

Where fileInfoHeaders contains the W3C ContentDisposition HTTP Header, e.g. when returning a FileInfo, ServiceStack returns:

attachment;filename="file.xml";size={bytesLen};
creation-date={date};modification-date={date};read-date={date};

To download a binary file you can use:

var rawBytes = downloadUrl.GetBytesFromUrl(httpRes => ...);


回答2:

I had a similar requirement which also required me to track progress of the streaming file download. I did it roughly like this:

server-side:

service:

public object Get(FooRequest request)
{
    var stream = ...//some Stream
    return new StreamedResult(stream);
}

StreamedResult class:

public class StreamedResult : IHasOptions, IStreamWriter
{
    public IDictionary<string, string> Options { get; private set; }
    Stream _responseStream;

    public StreamedResult(Stream responseStream)
    {
        _responseStream = responseStream;

        long length = -1;
        try { length = _responseStream.Length; }
        catch (NotSupportedException) { }

        Options = new Dictionary<string, string>
        {
            {"Content-Type", "application/octet-stream"},
            { "X-Api-Length", length.ToString() }
        };
    }

    public void WriteTo(Stream responseStream)
    {
        if (_responseStream == null)
            return;

        using (_responseStream)
        {
            _responseStream.WriteTo(responseStream);
            responseStream.Flush();
        }
    }
}

client-side:

string path = Path.GetTempFileName();//in reality, wrap this in try... so as not to leave hanging tmp files
var response = client.Get<HttpWebResponse>("/foo/bar");

long length;
if (!long.TryParse(response.GetResponseHeader("X-Api-Length"), out length))
    length = -1;

using (var fs = System.IO.File.OpenWrite(path))
    fs.CopyFrom(response.GetResponseStream(), new CopyFromArguments(new ProgressChange((x, y) => { Console.WriteLine(">> {0} {1}".Fmt(x, y)); }), TimeSpan.FromMilliseconds(100), length));

The "CopyFrom" extension method was borrowed directly from the source code file "StreamHelper.cs" in this project here: Copy a Stream with Progress Reporting (Kudos to Henning Dieterichs)

And kudos to mythz and any contributor to ServiceStack. Great project!



回答3:

I have found mythz answer to work well, but it is also possible to use their built in JSonServiceClient to also process file requests as well, just in a slightly non-intuitive way because you can't actually use the return type you would expect.

For a model definition like this:

[Route("/filestorage/outgoing/{Name}.{Extension}", "GET")]
[Route("/filestorage/outgoing", "GET")]
public class GetFileStorageStream : IReturn<HttpResult>
{
    public string Name { get; set; }
    public string Extension { get; set; }
    public bool ForDownload { get; set; }
}

You can define your service to return an HttpResult:

public class FileStorageService : Service
{
    public HttpResult Get(GetFileStorageStream fileInformation)
    {
        var internalResult = GetFromFileStorage(fileInformation);
        var fullFilePath = Path.Combine("C:\Temp", internalResult.FileName);
        return new HttpResult(new FileInfo(fullFilePath), asAttachment: fileInformation.ForDownload);
    }
}

Then on the client side you can use this Get template to properly get the web context:

var client = new JsonServiceClient("http://localhost:53842");
var httpResponse = client.Get<HttpWebResponse>("/filestorage/outgoing/test.jpg");
pictureBox1.Image = Image.FromStream(httpResponse.GetResponseStream());

I found it was not possible to use the new API Get methods as they would attempt to deserialize the HttpResult which isn't actually a true return type but a class representing the web context that service stack has created.



回答4:

You can intercept the response prior to it being handled by using a response filter, like below:

ServiceClientBase.HttpWebResponseFilter = response =>
{
    if (response.Headers["Content-Disposition"] != null)
    {
        var t = response.DownloadText();
        Console.WriteLine(t);
    }
};

However, this is not the best way to handle it, since the actual call to client.Method() will result in an ArgumentException when the client attempts to read the response stream (since it has been read previously by response.DownloadFile(...). I haven't yet figured out a way to handle it gracefully, but I 'll update my answer if I do.