ASP.NET Core 2 使用 HttpClientFactory 讀取中央氣象局 OpenData 的XML 下載

這是使用 ASP.NET Core 2 來讀取 CWB 的開放資料的 XML 檔,並讓 Client 端下載方式

註冊CWB會員

  1. CWB OpenData 平台(以下稱CWBODP)取用資料必須先註冊會員→ 這裡註冊
  2. 然後,登入OpenData平台→這裡登入
  3. 選擇『資料選擇說明』頁面,在下方可以找到『取得授權碼』,這個頁面下有使用教學
  4. 要取得資料代碼可以從『資料清單』頁面取得需要的代碼

有了上述資訊後,就可以著手開始做 .NET Core 2 的取用程式了。

CWODP取用 XML 檔案的 URI:
http://opendata.cwb.gov.tw/opendataapi?dataid={資料代碼}&authorizationkey={授權碼}

.NET Core 2 使用 HttpClient 有以下幾種:

  1. 一般 HttpClient 方式
  2. 直接使用 HttpClientFactory
  3. 具名式 Client
  4. 型別式 Client
  5. 產生式 Client

一般 HttpClient 方式

FileController.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http;

namespace CWBReader.Controllers
{
    [Route("[controller]")]
    public class FileController : Controller
    {
        [Route("")]
        public async Task<string> DownloadFile()
        {
            using (var client = new HttpClient())
            {
                string url = "http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678";
                using (var result = await client.GetAsync(url))
                {
                    if (result.IsSuccessStatusCode)
                    {
                        //直接將XML內容顯示出來
                        return await result.Content.ReadAsStringAsync();
                    }
                }
            }
            throw new Exception("File/can not show XML data!");
        }
    }
}


上面這段是展示如何使用標準 HttpClient 向 CWBODP 取得XML檔案,並顯示在畫面上。

但是如果要變成可以讓使用者端下載,就必須取得原本的檔案名稱資訊,並將返回資訊改成 FileResult 型別返回。如下:

FileController.cs
namespace CWBReader.Controllers
{
         [Route("download")]
        public async Task<FileResult> DownFile()
        {
            using (var client = new HttpClient())
            {
                string url = "http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678";
                using (var result = await client.GetAsync(url))
                {
                    if (result.IsSuccessStatusCode)
                    {
                        //以檔案下載方式輸出,取得原檔案名,並改以FileResult輸出
                        string filename = result.Content.Headers.ContentDisposition.FileName;
                        byte[] xml = await result.Content.ReadAsByteArrayAsync();
                        return File(xml, "application/xml", filename);
                    }
                }
            }
            throw new Exception("File/download can not download file!");
        }
    }
}


上面標準方式使用起來沒有問題,使用人數少的情況下也都還好,但是在很多人使用情況下下 HttpClient 就會被反覆的建立/銷毀,這個過程不但浪費時間也消耗資源,而且最嚴重的問題是使用 using 套接 HttpClient 實作時,離開 using 區段後 HttpClient 也沒有被及時銷毀,多人重複使用會導致通訊端口被占用到資源耗盡為止。

第二個問題是 HttpClient 不會理會 DNS 的有效時間 (TTL),可能發生了 DNS 變更了 IP 內容,卻還是繼續使用舊的IP對應。

可參考下面文章:



HttpClientFactory
於是乎,ASP.NET Core 2.1 以後推出了 HttpClientFactory ,它用來集中管理使用的 HttpClient ,這樣可以有效減少重構 HttpClient ,也可以減少資源浪費,重點也解決 DNS 本來就有 TTL 的問題。

直接使用 HttpClientFactory

要使用 HttpClientFactory 方法首先作成 DI 服務

startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            // 使用HttpClientFactory
            services.AddHttpClient();
            services.AddMvc();
         
        }


然後,在使用時取用該服務

CWBController .cs
namespace CWBReader.Controllers
{
    [Route("[controller]")]
    public class CWBController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public CWBController(IHttpClientFactory httpClientFactory)
        {
            // 取用HttpClientFactory 的服務
            _httpClientFactory = httpClientFactory;
        }

        [Route("t1")]
        public async Task<FileResult> GetByFctory1()
        {
            HttpClient client = _httpClientFactory.CreateClient();
            string uri = "http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678";
            using (var result = await client.GetAsync(uri))
            {
                if (result.IsSuccessStatusCode)
                {
                    // 取得XML內容
                    byte[] content = await result.Content.ReadAsByteArrayAsync();
                    // 取得下載檔案名稱
                    string filename = result.Content.Headers.ContentDisposition.FileName;
                    FileContentResult response = File(content, "application/xml", filename);
                    return response;
                }
            }
            throw new Exception("CWB/t1 can not download file!");
        }
    }
}


具名式 Client

在建立 DI 時就賦予特定名稱,然後取用時,只要指定名稱就可以取用套好的內容,通常是經常性且固定的取用會很方便。

startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
           // 使用固定內容,並賦予指定名稱 CWB
            services.AddHttpClient("CWB", c =>
            {
                c.BaseAddress = new Uri("http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678");
                c.DefaultRequestHeaders.Add("Accept", "*/*");
                c.DefaultRequestHeaders.Add("User-Agent" , "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1");
            });
            services.AddMvc();
         
        }


然後取用具名服務

CWBController .cs
namespace CWBReader.Controllers
{
    [Route("[controller]")]
    public class CWBController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public CWBController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        [Route("t2")]
        public async Task<FileResult> GetByFctory2()
        {
            // 在這裡取用具名 CWB
            HttpClient client = _httpClientFactory.CreateClient("CWB");
         
            using (var result = await client.GetAsync(""))
            {
                if (result.IsSuccessStatusCode)
                {
                    byte[] content = await result.Content.ReadAsByteArrayAsync();
                    string filename = result.Content.Headers.ContentDisposition.FileName;
                    FileContentResult response = File(content, "application/xml", filename);
                    return response;
                }
            }
            throw new Exception("CWB/t2 下載不到檔案");
        }
    }
}


型別式 Client

這種方式好處是可以先定義好很多不同需求的 Client 類別,灌入 DI 後,以Client形式來取用

CWBClient.cs
using System;
using System.Net.Http;

namespace CWBReader.Clients
{
    public class CWBClient
    {
        public HttpClient Client { get; private set; }

        public CWBClient(HttpClient client)
        {
            client.BaseAddress = new Uri("http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678");
            client.DefaultRequestHeaders.Add("Accept", "*/*");
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1");
            this.Client = client;
        }
    }
}


在 startup.cs 注入 DI 服務
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpClient<CWBClient>();
            services.AddMvc();
         
        }


然後取用

CWB2Controller.cs
namespace CWBReader.Controllers
{
    [Route("[controller]")]
    public class CWB2Controller : Controller
    {
        private readonly CWBClient _cwbClient;

        public CWB2Controller(CWBClient cwbClient)
        {
            // 取用定義好的 Client
            _cwbClient = cwbClient;
        }
     
        [Route("t1")]
        public async Task<FileResult> GetByClient1()
        {
            using (var result = await _cwbClient.Client.GetAsync(""))
            {
                if (result.IsSuccessStatusCode)
                {
                    byte[] content = await result.Content.ReadAsByteArrayAsync();
                    string filename = result.Content.Headers.ContentDisposition.FileName;
                    FileContentResult response = File(content, "application/xml", filename);
                    return response;
                }
            }
            throw new Exception("CWB2/t1 can not download file");
        }
    }
}


產生式 Client

這種方式可以封裝整個 HttpClient ,也可以由第三方套件來提供,只要有套件方的介面即可實現。因為此處我沒有第三方套件,所以先由自己做。

首先產生一個介面

ICWBClient.cs
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace CWBReader.Clients
{
    public interface ICWBClient
    {
        Task<FileResult> GetFile();
    }
}


然後建立一個具有介面特徵的類別(被封裝者)

CWBHubClient.cs
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace CWBReader.Clients
{
    public class CWBHubClient :ICWBClient
    {
        private readonly HttpClient _client;

        public CWBHubClient(HttpClient client)
        {
            client.BaseAddress = new Uri("http://opendata.cwb.gov.tw/opendataapi?dataid=F-C0032-001&authorizationkey=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678");
            client.DefaultRequestHeaders.Add("Accept", "*/*");
            client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1");
            _client = client;
        }

        public async Task<FileResult> GetFile()
        {
            using (var result = await _client.GetAsync(""))
            {
                if (result.IsSuccessStatusCode)
                {
                    byte[] content = await result.Content.ReadAsByteArrayAsync();
                    string filename = result.Content.Headers.ContentDisposition.FileName;
                    FileContentResult response = new FileContentResult(content, "application/xml") { FileDownloadName = filename };
                    return response;
                }
            }
            throw new Exception("CWB2/t2 下載不到檔案");
        }
    }
}


注入DI

startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpClient<ICWBClient, CWBHubClient>();
            services.AddMvc();
         
        }


呼叫引用

CWB3Controller.cs
namespace CWBReader.Controllers
{
    [Route("[controller]")]
    public class CWB3Controller : Controller
    {
        private readonly ICWBClient _cwbClient;

        public CWB3Controller(ICWBClient cwbClient)
        {
            _cwbClient = cwbClient;
        }

        [Route("t1")]
        public Task<FileResult> GetByClient1()
        {
            return _cwbClient.GetFile();
        }
    }
}


將 Http 服務封裝注入 DI 好處是除了這些控制器可以使用外,也可以用在 Razor 網頁上,提供不少便利。


參考相關:




留言

這個網誌中的熱門文章

【研究】列印的條碼為什麼很難刷(掃描)

C# 使用 Process.Start 執行外部程式

統一發票列印小程式