65.9K
CodeProject 正在变化。 阅读更多。
Home

Web API 想法 1/3 - 数据流式传输

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (202投票s)

2014年11月4日

CPOL

8分钟阅读

viewsIcon

248270

downloadIcon

4356

与 ASP.NET Web API 相关的项目。通过关于数据流、使用 HTTPS 和扩展 Web API 文档的文章,涉及该技术的大部分方面。

背景

关于 ASP.NET Web API 技术有大量的文章。在 CodeProject (ASP.NET Web API)、ASP.NET Web API 官方网站以及其他地方都可以找到很多。以下一系列文章将尝试通过应用数据流、HTTPS 和扩展 Web API 文档来解释该技术。文章假设您具备 **C#、ASP.NET、OOP、ASP.NET MVC、REST 服务、IIS 以及 .NET Framework 的一些技术术语** 的基本知识。话虽如此,文章将按以下顺序呈现。
  1. 引言
  2. 数据流
  3. 使用 HTTPS
  4. 扩展 Web API 文档

需求和依赖项

  • 该解决方案最好使用 VS 2012 (SP4) Ultimate 及更高版本查看。但也可以使用 VS Professional 或 Express 2012 及更高版本查看每个单独的项目。
  • .NET Framework 4.5.1 及更高版本
  • 大量 Nuget 包。请参阅每个项目的 Nuget **packages.config** 文件

文章将涵盖的内容

  • 与 Asp.NET Web API 技术相关
    1. ActionFilter
    2. AuthorizationFilter
    3. DelegateHandler
    4. 不同的 Web API 路由属性
    5. MediaTypeFormatter
    6. OWIN
    7. 自托管
    8. Web API 文档及其扩展
  • 与 .NET Framework 及其他相关
    1. Async/Await
    2. .NET 反射
    3. 序列化
    4. ASP.NET Web API/MVC 错误处理
    5. IIS、HTTPS 和证书
    6. 设计原则和技术

引言

自 ASP.NET Web API 添加到 .NET Framework 以来已有多年。ASP.NET Web API 构建在 HTTP 协议之上,该协议将 RESTful 服务和数据暴露给外部系统。目标是利用 HTTP 技术将服务扩展到各种平台,而 HTTP 技术得到了许多最终用户应用程序、浏览器、移动设备和其他服务的支持。ASP.NET Web API 是一种请求-响应消息交换模式,其中客户端可以从服务器请求某些信息,服务器将请求响应给客户端。响应可以同步或异步进行。当我们想到 Web API 时,通常会想到几件事。我个人会指出这三个基本要点,而忽略 Web API 的实现。

  • 服务及其方法的目的。
  • 每个方法的输入(即请求)
  • 每个方法的输出(即响应)。

根据约定,ASP.NET Web API 允许您定义与 HTTP 方法(动词)具有匹配语义的方法。例如,如果我们有一个名为 `GetPysicians()` 的方法,那么它与 HTTP 方法的匹配名称将是 `GET`。简而言之,下面的图表显示了每个方法与 HTTP 方法(动词)的匹配情况。

但这种方法可能不适用于不同场景。例如,您可能希望在单个 API 控制器类中定义多个 `GET` 或 `POST` 方法。在这种情况下,框架允许您定义一个动作级路由,将动作包含在 URI 请求中。以下代码显示了如何配置这一点。

 
public static void Register(HttpConfiguration config)
{
    // Web API configuration and services
    // Web API routes
     config.MapHttpAttributeRoutes();
    
     config.Routes.MapHttpRoute(name: "PhysicianApi",
                routeTemplate: "{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional });
}

但这仍然可能不足以满足其他场景。假设您想要一个方法来从集中式存储库中删除文件,并且您想使用同一方法来获取文件。在这种情况下,Web API 框架允许您使用 `Get` 和 `Delete` HTTP 方法属性来装饰该动作。请参见下图。

请注意,`RemoveFile` 方法可以通过 Delete (`HttpDelete`) 或 Get (`HttpGet`) 动词调用。HTTP 方法属性也有助于为服务方法定义合适的名称。

框架提供的另一个功能是能够提供属性路由模板。这类似于 ASP.NET MVC 路由,不同之处在于它依赖于 HTTP 方法(动词)而不是动作的 URI。这允许您在 Web API 服务下定义几种类型的动作。例如,您可以定义一个带参数的 URI 服务方法。因此,当对服务方法发出请求时,您可以将参数作为请求 URI 的一部分。以下示例代码显示了 `GetPhysicianBySpeciality` 方法上的 Web API 属性路由。

[Route("physician/{speciality}/{physicianID}")]
public PhysicianBase GetPhysicianBySpeciality(string speciality, int physicianID)
{
    // Todo : Process the request 
}

这样,客户端就可以发送类似 https:///physician/eyecare/1 的请求。有各种类型的路由属性有助于装饰 Web API 控制器及其方法。它们是

ActionName 允许您定义动作名称路由
HTTP 方法(HttpGet、HttpPost、AcceptVerbs ...) 允许您定义 HTTP 方法(动词)
NonAction 阻止调用该动作
路线 允许您定义带/不带参数的模板
RoutePrefix 允许您定义控制器前缀名称

ASP.NET Web API 还包含 `HttpClient`、`HttpRequestMessage` 和 `HttpResponseMessage` 等有用的库和类。请参阅参考部分以获取 Web API 引用的列表。这对于介绍来说已经足够了。在下一节中,我将解释文章的一个主要领域。

数据流

通过互联网执行的频繁操作之一是数据流。ASP.NET Web API 能够处理来自/到服务器/客户端的大量流数据。流数据可以是目录中的文件,也可以是存储在数据库中的二进制数据。在本文中,有两个基本方法,即 `Download` 和 `Upload`,用于完成数据流。下载负责从服务器拉取流数据,而上传负责将流数据保存到服务器。

注意:本文档说明了位于特定(可配置)目录的资源的数据流,并通过 Web API 服务进行流式传输。

参与的项目

  • WebAPIDataStreaming
  • WebAPIClient
  • POCOLibrary

在解释代码部分之前,需要在 IIS (7.5) 和 Web API 服务 web.config 文件中进行某些配置。

  1. 确保 Downloads/Uploads 目录/文件已授予用户(IIS_IUSRS)必要的权限(读取/写入)。
  2. 确保有足够的内存(RAM)和硬盘空间来处理大文件。
  3. 对于较大的文件数据,
    1. 确保在 _web.config_ 文件中配置了 `maxRequestLength` 以及合理的 `executionTimeout`。该值可能取决于允许流式传输的大小。允许的最大文件大小为 2GB。

    2. 确保在 web.config 文件的 _requestFiltering_ 配置部分下配置了 `maxAllowedContentLength`。此设置的默认值约为 30MB,最大值为 4GB。

      注意:为了使用 `maxAllowedContentLength` 配置,需要在 IIS 上启用请求筛选功能。方法如下:
      • 转到“控制面板”
      • 在“程序和功能”菜单的左侧选择“打开或关闭 Windows 功能”
      • 选择“Internet Information Services” > “World Wide Web Services” > “Security”
      • 启用请求筛选

      请求筛选的详细信息可以在参考部分找到
完成这些预配置后,就可以轻松创建和使用数据流 Web API 服务。首先要做的是定义文件流 `ApiController` 以及必要的操作。正如我之前提到的,文件流服务的主要方法是下载和上传。但它也包含其他与文件流相关的方法。
/// <summary>
/// File streaming API
/// </summary>
[RoutePrefix("filestreaming")]
[RequestModelValidator]
public class StreamFilesController : ApiController
{
    /// <summary>
    /// Get File meta data
    /// </summary>
    /// <param name="fileName">FileName value</param>
    /// <returns>FileMeta data response.</returns>
    [Route("getfilemetadata")]
    public HttpResponseMessage GetFileMetaData(string fileName)
    {
        // .........................................
        // Full code available in the source control
        // .........................................

    }

    /// <summary>
    /// Search file and return its meta data in all download directories
    /// </summary>
    /// <param name="fileName">FileName value</param>
    /// <returns>List of file meta datas response</returns>
    [HttpGet]
    [Route("searchfileindownloaddirectory")]
    public HttpResponseMessage SearchFileInDownloadDirectory(string fileName)
    {
        // .........................................
        // Full code available in the source control
        // .........................................
    }

    /// <summary>
    /// Asynchronous Download file
    /// </summary>
    /// <param name="fileName">FileName value</param>
    /// <returns>Tasked File stream response</returns>
    [Route("downloadasync")]
    [HttpGet]
    public async Task<HttpResponseMessage> DownloadFileAsync(string fileName)
    {
        // .........................................
        // Full code available in the source control
        // .........................................
    }

    /// <summary>
    /// Download file
    /// </summary>
    /// <param name="fileName">FileName value</param>
    /// <returns>File stream response</returns>
    [Route("download")]
    [HttpGet]
    public HttpResponseMessage DownloadFile(string fileName)
    {
        // .........................................
        // Full code available in the source control
        // .........................................
    }

    /// <summary>
    /// Upload file(s)
    /// </summary>
    /// <param name="overWrite">An indicator to overwrite a file if it exist in the server</param>
    /// <returns>Message response</returns>
    [Route("upload")]
    [HttpPost]
    public HttpResponseMessage UploadFile(bool overWrite)
    {
        // .........................................
        // Full code available in the source control
        // .........................................
    }

    /// <summary>
    /// Asynchronous Upload file
    /// </summary>
    /// <param name="overWrite">An indicator to overwrite a file if it exist in the server</param>
    /// <returns>Tasked Message response</returns>
    [Route("uploadasync")]
    [HttpPost]
    public async Task<HttpResponseMessage> UploadFileAsync(bool overWrite)
    {
        // .........................................
        // Full code available in the source control
        // .........................................
    }
}

`Download` 服务方法首先检查请求的文件名是否存在于目标文件路径中。如果文件未找到,它将返回一个错误响应对象,提示“文件未找到”。如果成功,它会将内容读取为字节,并以 `application/octet-stream` MIME 内容类型附加到响应对象。

/// <summary>
/// Download file
/// </summary>
/// <param name="fileName">FileName value<param>
/// <returns>File stream response<returns>
[Route("download")]
[HttpGet]
public HttpResponseMessage DownloadFile(string fileName)
{
    HttpResponseMessage response = Request.CreateResponse();
    FileMetaData metaData = new FileMetaData();
    try
    {
        string filePath = Path.Combine(this.GetDownloadPath(), @"\", fileName);
        FileInfo fileInfo = new FileInfo(filePath);

        if (!fileInfo.Exists)
        {
            metaData.FileResponseMessage.IsExists = false;
            metaData.FileResponseMessage.Content = string.Format("{0} file is not found !", fileName);
            response = Request.CreateResponse(HttpStatusCode.NotFound, metaData, new MediaTypeHeaderValue("text/json"));
        }
        else
        {
            response.Headers.AcceptRanges.Add("bytes");
            response.StatusCode = HttpStatusCode.OK;
            response.Content = new StreamContent(fileInfo.ReadStream());
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            response.Content.Headers.ContentDisposition.FileName = fileName;
            response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            response.Content.Headers.ContentLength = fileInfo.Length;
        }
    }
    catch (Exception exception)
    {
        // Log exception and return gracefully
        metaData = new FileMetaData();
        metaData.FileResponseMessage.Content = ProcessException(exception);
        response = Request.CreateResponse(HttpStatusCode.InternalServerError, metaData, new MediaTypeHeaderValue("text/json"));
    }
    return response;
}

`Upload` 服务方法基于 `multipart/form-data` MIME 内容类型工作。首先,它检查 HTTP 请求内容类型是否为 multipart 类型。如果成功,它会将内容长度与允许上传的最大文件大小进行比较。如果成功,它将开始将请求内容上传到目标位置。操作完成后,它会向用户发送适当的响应消息。执行上传的代码片段如下所示。

/// <summary>
/// Upload file(s)
/// </summary>
/// <param name="overWrite">An indicator to overwrite a file if it exist in the server.</param>
/// <returns>Message response</returns>
[Route("upload")]
[HttpPost]
public HttpResponseMessage UploadFile(bool overWrite)
{
    HttpResponseMessage response = Request.CreateResponse();
    List<FileResponseMessage> fileResponseMessages = new List<FileResponseMessage>();
    FileResponseMessage fileResponseMessage = new FileResponseMessage { IsExists = false };

    try
    {
        if (!Request.Content.IsMimeMultipartContent())
        {
            fileResponseMessage.Content = "Upload data request is not valid !";
            fileResponseMessages.Add(fileResponseMessage);
            response = Request.CreateResponse(HttpStatusCode.UnsupportedMediaType, fileResponseMessages, new MediaTypeHeaderValue("text/json"));
        }

        else
        {
            response = ProcessUploadRequest(overWrite);
        }
    }
    catch (Exception exception)
    {
        // Log exception and return gracefully
        fileResponseMessage = new FileResponseMessage { IsExists = false };
        fileResponseMessage.Content = ProcessException(exception);
        fileResponseMessages.Add(fileResponseMessage);
        response = Request.CreateResponse(HttpStatusCode.InternalServerError, fileResponseMessages, new MediaTypeHeaderValue("text/json"));

    }
    return response;
}

/// <summary>
/// Asynchronous Upload file
/// </summary>
/// <param name="overWrite">An indicator to overwrite a file if it exist in the server.<param>
/// <returns>Tasked Message response</returns>
[Route("uploadasync")]
[HttpPost]
public async Task<HttpResponseMessage> UploadFileAsync(bool overWrite)
{
    return await new TaskFactory().StartNew(
       () =>
       {
           return UploadFile(overWrite);
       });
}

/// <summary>
/// Process upload request in the server
/// </summary> 
/// <param name="overWrite">An indicator to overwrite a file if it exist in the server.</param>
/// </returns>List of message object</returns>
private HttpResponseMessage ProcessUploadRequest(bool overWrite)
{
    // .........................................
    // Full code available in the source control
    // .........................................
}

调用下载和上传文件方法的客户端应用程序是一个控制台应用程序。该应用程序通过 `HttpClient` 和相关类使用文件流 Web API 服务。基本上,下载文件代码会创建一个带有正确文件名的下载 HTTP 请求对象,并将请求发送到服务器。

/// <summary>
/// Download file
/// </summary>
/// <returns>Awaitable Task object</returns>
private static async Task DownloadFile()
{
    Console.ForegroundColor = ConsoleColor.Green;
    Console.WriteLine("Please specify file name  with extension and Press Enter :- ");
    string fileName = Console.ReadLine();
    string localDownloadPath = string.Concat(@"c:\", fileName); // the path can be configurable
    bool overWrite = true;
    string actionURL = string.Concat("downloadasync?fileName=", fileName);

    try
    {
        Console.WriteLine(string.Format("Start downloading @ {0}, {1} time ",
            DateTime.Now.ToLongDateString(),
            DateTime.Now.ToLongTimeString()));


        using (HttpClient httpClient = new HttpClient())
        {
            httpClient.BaseAddress = baseStreamingURL;
            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, actionURL);

            await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).
                ContinueWith((response)
                    =>
                {
                    Console.WriteLine();
                    try
                    {
                        ProcessDownloadResponse(localDownloadPath, overWrite, response);
                    }
                    catch (AggregateException aggregateException)
                    {
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine(string.Format("Exception : ", aggregateException));
                    }
                });
        }
    }
    catch (Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(ex.Message);
    }
}


/// <summary>
/// Process download response object
/// </summary>
/// <param name="localDownloadFilePath">Local download file path</param>
/// <param name="overWrite">An indicator to overwrite a file if it exist in the client.</param>
/// <param name="response">Awaitable HttpResponseMessage task value</param>
private static void ProcessDownloadResponse(string localDownloadFilePath, bool overWrite,
    Task<HttpResponseMessage> response)
{
    if (response.Result.IsSuccessStatusCode)
    {
        response.Result.Content.DownloadFile(localDownloadFilePath, overWrite).
            ContinueWith((downloadmessage)
                =>
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(downloadmessage.TryResult());
            });
    }
    else
    {
        ProcessFailResponse(response);
    }
}

请注意上面的代码。`HttpClient` 对象发送请求并等待响应,以便仅发送响应头(`HttpCompletionOption.ResponseHeadersRead`),而不是文件的整个响应内容。一旦读取了响应头,它将对内容执行必要的验证,如果成功,将执行实际的文件下载方法。

这是调用上传文件流 Web API 服务方法的代码。与下载方法类似,它会创建一个带有 multipart 表单数据内容类型的请求对象,并将请求发送到服务器。内容经过验证并发送到服务器以进行进一步处理。

/// <summary>
/// Upload file
/// </summary>
/// <returns>Awaitable task object</returns>
private static async Task UploadFile()
{
    try
    {
        string uploadRequestURI = "uploadasync?overWrite=true";

        MultipartFormDataContent formDataContent = new MultipartFormDataContent();

        // Validate the file and add to MultipartFormDataContent object
        formDataContent.AddUploadFile(@"c:\nophoto.png");
        formDataContent.AddUploadFile(@"c:\ReadMe.txt");

        if (!formDataContent.HasContent()) // No files found to be uploaded
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.Write(formDataContent.GetUploadFileErrorMesage());
            return;
        }
        else
        {
            string uploadErrorMessage = formDataContent.GetUploadFileErrorMesage();
            if (!string.IsNullOrWhiteSpace(uploadErrorMessage)) // Some files couldn't be found
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write(uploadErrorMessage);
            }

            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uploadRequestURI);
            request.Content = formDataContent;

            using (HttpClient httpClient = new HttpClient())
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine(string.Format("Start uploading @ {0}, {1} time ",
                DateTime.Now.ToLongDateString(),
                DateTime.Now.ToLongTimeString()));

                httpClient.BaseAddress = baseStreamingURL;
                await httpClient.SendAsync(request).
                      ContinueWith((response)
                          =>
                          {
                              try
                              {
                                  ProcessUploadResponse(response);
                              }
                              catch (AggregateException aggregateException)
                              {
                                  Console.ForegroundColor = ConsoleColor.Red;
                                  Console.WriteLine(string.Format("Exception : ", aggregateException));
                              }
                          });
            }
        }
    }
    catch (Exception ex)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(ex.Message);
    }
} 

/// <summary>
/// Process download response object
/// </summary>
/// <param name="response">Awaitable HttpResponseMessage task value</param>
private static void ProcessUploadResponse(Task<HttpResponseMessage> response)
{
    if (response.Result.IsSuccessStatusCode)
    {
        string uploadMessage = string.Format("\nUpload completed @ {0}, {1} time ",
                    DateTime.Now.ToLongDateString(),
                    DateTime.Now.ToLongTimeString());
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine(string.Format("{0}\nUpload Message : \n{1}", uploadMessage,
            JsonConvert.SerializeObject(response.Result.Content.ReadAsAsync<List<FileResponseMessage>>().TryResult(), Formatting.Indented)));
    }
    else
    {
        ProcessFailResponse(response);
    }
}

数据流项目还包含未在本文中明确解释的扩展类和方法。下载源代码并进行探索。

下一步

下一节将介绍 使用 HTTPS

参考文献

历史和 GitHub 版本

© . All rights reserved.