最近在改進園子的圖片上傳程序,希望實現用戶上傳圖片時同時將圖片文件保存在三個地方:1)服務器本地硬盤;2)又拍雲;3)阿里雲OSS。並且在保存時使用異步操作。
對於異步保存到本地硬盤,只需用 Steam.CopyToAsync() 將上傳文件流異步復制到 FileStream 即可。
對於異步保存至又拍雲,只要借助 WebRequest.GetRequestStreamAsync() + Steam.CopyToAsync() 就可以實現。
而阿里雲OSS提供了 .NET SDK,使用起來很方便,但是之前並沒有提供異步接口,成為異步化的一個障礙。
今天在 OSS .NET SDK 的更新日志中驚喜地發現:“添加異步化接口(支持Put/Get/List/Copy/PartCopy等異步操作)”。於是立馬下載下來,可是一使用驚喜瞬間化為烏有 —— 新版 SDK 只提供了傳統的 Begin/End 異步接口,卻沒有提供 async 異步接口。
public IAsyncResult BeginPutObject(string bucketName, string key, Stream content, AsyncCallback callback, object state);
public PutObjectResult EndPutObject(IAsyncResult asyncResult);
懷着失落的心情,望着孤零零的沒有 await 陪伴的 async,心里有說不出的滋味。。。
async Task<bool> IBucket.PutFileAsync(string filePath, Stream uploadStream) { filePath = filePath.Substring(1); uploadStream.Position = 0; _client.PutObject(_bucketName, filePath, uploadStream); return true; }
難道這次只能實現半吊子的異步化嗎?好不容易等來 OSS .NET SDK 支持異步化,難道只是空歡喜一場嗎?真有些不甘心啊!
這時,心中突然閃過一個念頭:有沒有可能直接用 async/await 調用 Begin/End 異步方法?也許微軟早就為我們准備好了餡餅?
於是,在網上搜尋了一番,發現了一線希望 —— 用 Task.Factory.FromAsync() 是可能實現的。
可是,一堆 FromAsync 方法看着就讓人暈,只能一點點去試。
開始用的是 Task.Factory.FromAsync<PutObjectResult> ,但參數總對不上,比如:
await Task.Factory.FromAsync<PutObjectResult>( _client.BeginPutObject, _client.EndPutObject, _bucketName, filePath, uploadStream, null);
編譯出錯:
No overload for method 'FromAsync' takes 6 arguments
后來改為下面這樣,總算編譯通過了:
await Task.Factory.FromAsync<PutObjectResult>( _client.BeginPutObject(_bucketName, filePath, uploadStream, null, null), x => { return _client.EndPutObject(x); });
而且運行程序,圖片都能成功上傳到阿里雲OSS中,但總是報這樣的錯誤:
failed: System.ArgumentException : retryableAsyncResult should not be null at Aliyun.OpenServices.OpenStorageService.Utilities.OssUtils.EndOperationHelper(IServiceClient serviceClient, IAsyncResult asyncResult)
這個錯誤說明 callback 調用沒成功。
在這個地方折騰了很長時間,后來瞎貓碰着死耗子,把 Task.Factory.FromAsync<PutObjectResult> 改為 Task<PutObjectResult>.Factory.FromAsync 問題就解決了。代碼如下:
async Task<bool> IBucket.PutFileAsync(string filePath, Stream uploadStream) { filePath = filePath.Substring(1); uploadStream.Position = 0; var result = await Task<PutObjectResult>.Factory.FromAsync( _client.BeginPutObject, _client.EndPutObject, _bucketName, filePath, uploadStream, null); Console.WriteLine(result.ETag); return true; }
【2017-7-25更新】
感謝評論中 @唐詩 的建議!TaskCompletionSource 的確是更好的解決方法,最新的實現代碼如下:
Task<bool> IBucket.PutFileAsync(string filePath, Stream uploadStream, string contentType) { filePath = filePath.Substring(1); uploadStream.Position = 0; var tcs = new TaskCompletionSource<bool>(); _client.BeginPutObject(_bucketName, filePath, uploadStream, new ObjectMetadata { ContentType = contentType }, asyncResult => { _client.EndPutObject(asyncResult); tcs.SetResult(true); }, null); return tcs.Task; }