.net core c#

Mask Query API

Matt 2020/04/13 15:20:40
1421

As these days' situations in the whole world cause by Coronavirus Disease 2019, aka COVID-19, or 武漢肺炎 (Thanks to every Government officials, and other Releated Heroes in Taiwan, did their duty for people's health, and life.). Wearing a mask outside in public place is an important things now.

According to a friend's post: 簡單四步驟:使用ASP.NET Core提供口罩剩餘數量查詢API, we will build a mask query api but something different.

Here we go, let's do it.

Create a Model

First, based on 健保特約機構口罩剩餘數量明細清單, an official open data source from government.

img "資料資源欄位: 醫事機構代碼、醫事機構名稱、醫事機構地址、醫事機構電話、成人口罩剩餘數、兒童口罩剩餘數、來源資料時間"

public class MaskInfo
{
    /// <summary>
    /// 醫事機構代碼
    /// </summary>
    [Index(0)]
    public string Code { get; set; }

    /// <summary>
    /// 醫事機構名稱
    /// </summary>
    [Index(1)]
    public string Name { get; set; }

    /// <summary>
    /// 醫事機構地址
    /// </summary>
    [Index(2)]
    public string Address { get; set; }

    /// <summary>
    /// 醫事機構電話
    /// </summary>
    [Index(3)]
    public string Phone { get; set; }

    /// <summary>
    /// 成人口罩剩餘數
    /// </summary>
    [Index(4)]
    [JsonPropertyName("adults")]
    public int AdultRemainings { get; set; }

    /// <summary>
    /// 兒童口罩剩餘數
    /// </summary>
    [Index(5)]
    [JsonPropertyName("child")]
    public int ChildRemainings { get; set; }

    /// <summary>
    /// 來源資料時間
    /// </summary>
    [Index(6)]
    [JsonPropertyName("time")]
    public string ResourceTime { get; set; }
}

An attribute Index(0) presents. For now, we just ignore that thing, I will explain it later.

Create a Service

Our goal is making a service directly read from the data source of .csv file. Why .csv ? In this case, my consideration of json that is too large to transmit via internet.

A data source of 733KB file (.csv) vs. 10.5MB file (.json), what will you choose ?

So that, we will need CsvHelper to help us to deal with .csv easily.

img "CsvHelper from NuGet"

Things we mentioned earlier about Index(0), these are for CsvHelper models mapper, a model field maps to a specific csv column.

public class MaskService
{
    public readonly HttpClient _client;

    public MaskService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://data.nhi.gov.tw/");
        this._client = client;
    }

    public async Task<IEnumerable<MaskInfo>> GetMasks()
    {
        var resp = await this._client.GetAsync("resource/mask/maskdata.csv").ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();

        using var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
        // using var stream = new FileStream(@"x:\maskdata.csv", FileMode.Open, FileAccess.Read);
		
        using var reader = new StreamReader(stream);
        using var csv = new CsvReader(reader, CultureInfo.CurrentCulture);
        csv.Configuration.HasHeaderRecord = true;
        csv.Configuration.ShouldSkipRecord = r => r[0] == "醫事機構代碼";
        csv.Configuration.IgnoreBlankLines = true;
		
        // codes for model mapping, separating these parts into other class (file) to deal with it will be a good idear.
        var masks = new List<MaskInfo>();
        while (await csv.ReadAsync().ConfigureAwait(false))
        {
            csv.TryGetField<string>(0, out var code);
            csv.TryGetField<string>(1, out var name);
            csv.TryGetField<string>(2, out var addr);
            csv.TryGetField<string>(3, out var phone);
            csv.TryGetField<int>(4, out var adults);
            csv.TryGetField<int>(5, out var childred);
            csv.TryGetField<string>(6, out var time);

            masks.Add(new MaskInfo
            {
                Code = code,
                Name = name,
                Address = addr,
                Phone = phone,
                AdultRemainings = adults,
                ChildRemainings = childred,
                ResourceTime = time
            });
        }
        return masks;

        // return csv.GetRecords<MaskInfo>();
    }
}

According to CsvHelper document, the model mapping block should be completed by one line code,

return csv.GetRecords<MaskInfo>();

Unfortunately, I cannot figure it out that code won't work correctly, 'cause the yield retuen will break after 29 records read. However, we will worry nothing, we still can do model mapping manually.

Register Service

For the api controller calling, we need to register HttpClient in Startup first.

services.AddHttpClient<MaskService>();

It will inject HttpClient instance into MaskService when we need the service.

Implement Api Controller

Final step. We build an api for 3rd party using.

[Route("api/[controller]")]
[ApiController]
public class MaskController : ControllerBase
{
    private readonly MaskService _maskService;

    public MaskController(MaskService maskService)
    {
        this._maskService = maskService;
    }
	
    [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        try
        {
            var data = await this._maskService.GetMasks().ConfigureAwait(false);
            return Ok(data);
        }
        catch
        {
            throw;
        }
    }
}

When api called, we will see the mask info results.

img "api call"

I believe that you noticed a ResponseCache attribute set for the action. It's a good strategy to prevent from people call api frequently.

One thing to go, it's quite easy to build an api service by using ASP.Net core.

Enjoy it.


References:

簡單四步驟:使用ASP.NET Core提供口罩剩餘數量查詢API

簡單五步驟:以EF Core整合SQLite儲存口罩剩餘數量資訊

答客問:中文欄位對輸出資料長度的影響?

健保特約機構口罩剩餘數量明細清單

TextReader Class

Create a web API with ASP.NET Core

新冠肺炎即時地圖

Matt