這算是個練習的題目,原本覺得應該是個簡單的應用,沒想到實做下去遇到了幾個進階的操作,還蠻有趣的,用的是 ASP.NET Core WebAPI,但如果要用在 ASP.NET WebAPI 專案裡也是可以的,程式的部分並不會有多大的差異。寫程式的過程中還發想出不少的延伸應用情境,所以之後會有幾篇文章跟這篇的內容有所關連。
這篇文章會提到的內容:GZipStream, JsonConvert, AutoMapper
YouBike 臺北市公共自行車即時資訊
http://data.taipei/opendata/datalist/datasetMeta?oid=8ef1626a-892a-4218-8344-f7ac46e1aa48
資料存取網址
https://tcgbusfs.blob.core.windows.net/blobyoubike/YouBikeTP.gz
檔案格式為經 gz 壓縮之 json 檔,請下載後解壓縮使用
主要欄位說明
sno:站點代號、 sna:場站名稱(中文)、 tot:場站總停車格、 sbi:場站目前車輛數量、 sarea:場站區域(中文)、 mday:資料更新時間、 lat:緯度、 lng:經度、 ar:地(中文)、 sareaen:場站區域(英文)、 snaen:場站名稱(英文)、 aren:地址(英文)、 bemp:空位數量、 act:全站禁用狀態
回傳成功的 JSON 內容 ( 經過解壓縮後 )
上面就是要拿來應用的原始資料,第一個會遇到的狀況是,原始資料並不是直接給我們 JSON 資料,是經過 gzip 壓縮過的資料,所以要拿到實際可以用的資料就必須要在程式裡先做解壓縮處理。而第二個狀況就是 JSON 裡每個場站的資料雖然是結構化的,但 retVal 裡並不是直接給場站資料的 collection,而是 Key, Value 資料的 Collection,然後每個 key 都是對應場站資料的 sno (站點代號) 值。
以往拿到 JSON 資料後,會使用 Visual Studio 的「編輯 > 選擇性貼上 > 貼上 JSON 做為類別」的功能,快速產生對映 JSON 資料的類別。但如果是直接拿上面的原始 YouBike 資料去使用「貼上 JSON 做為類別」這個功能,那麼就會……
直接產生一堆以 Key 為名稱的類別,然後每個類別的屬性成員都一模一樣,因為每個 YouBike 場站資料所對映的類別應該是同一個才對,而不是每一個場站都有自己的類別,這部分會讓開發者感到稍微棘手,解決的方式有很多,不過我使用的是比較簡單的做法。
實作
建立 ASP.NET Core WebAPI 專案
專案請加入 JSON.NET 的 NuGet Package
建立 Infrastructure 資料夾,底下再新增 Models 與 Service 資料夾
Repository 資料夾則是會放取得 YouBike 資料的類別Service 資料夾則是會放給 WebAPI Controller 取得 YouBike 資料的服務類別
Repository - 建立資料模型類別
YouBikeOriginalModel 類別
using System; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
namespace SampleWebApi.Infrastructure.Reposiotry | |
{ | |
public class YouBikeOriginalModel | |
{ | |
[JsonProperty("retCode")] | |
public int RetuenCode { get; set; } | |
[JsonProperty("retVal")] | |
public JObject ReturnValue { get; set; } | |
} | |
} |
YouBikeStationModel 類別
using System; | |
using Newtonsoft.Json; | |
namespace SampleWebApi.Infrastructure.Reposiotry | |
{ | |
public class YouBikeStationModel | |
{ | |
/// <summary> | |
/// 站點代號 | |
/// </summary> | |
[JsonProperty("sno")] | |
public string No { get; set; } | |
/// <summary> | |
/// 場站名稱(中文) | |
/// </summary> | |
[JsonProperty("sna")] | |
public string Name { get; set; } | |
/// <summary> | |
/// 場站總停車格 | |
/// </summary> | |
[JsonProperty("tot")] | |
public int Total { get; set; } | |
/// <summary> | |
/// 場站目前車輛數量 | |
/// </summary> | |
[JsonProperty("sbi")] | |
public int Bikes { get; set; } | |
/// <summary> | |
/// 場站區域(中文) | |
/// </summary> | |
[JsonProperty("sarea")] | |
public string Area { get; set; } | |
/// <summary> | |
/// 資料更新時間 | |
/// </summary> | |
[JsonProperty("mday")] | |
public string ModifyTime { get; set; } | |
/// <summary> | |
/// 緯度 | |
/// </summary> | |
[JsonProperty("lat")] | |
public double Latitude { get; set; } | |
/// <summary> | |
/// 經度 | |
/// </summary> | |
[JsonProperty("lng")] | |
public double Longitude { get; set; } | |
/// <summary> | |
/// 地址(中文) | |
/// </summary> | |
[JsonProperty("ar")] | |
public string Address { get; set; } | |
/// <summary> | |
/// 場站區域(英文) | |
/// </summary> | |
[JsonProperty("sareaen")] | |
public string AreaEnglish { get; set; } | |
/// <summary> | |
/// 場站名稱(英文) | |
/// </summary> | |
[JsonProperty("snaen")] | |
public string NameEnglish { get; set; } | |
/// <summary> | |
/// 地址(英文) | |
/// </summary> | |
[JsonProperty("aren")] | |
public string AddressEnglish { get; set; } | |
/// <summary> | |
/// 空位數量 | |
/// </summary> | |
[JsonProperty("bemp")] | |
public int BikeEmpty { get; set; } | |
/// <summary> | |
/// 禁用狀態 (0:禁用, 1:正常) | |
/// </summary> | |
[JsonProperty("act")] | |
public string Active { get; set; } | |
} | |
} |
Repository - 建立 YouBikeRepository 類別
IYouBikeRepository.cs
using System; | |
using System.Collections.Generic; | |
namespace SampleWebApi.Infrastructure.Reposiotry | |
{ | |
public interface IYouBikeRepository | |
{ | |
/// <summary> | |
/// 取得所有 YouBike 場站資料. | |
/// </summary> | |
/// <returns>List<YouBikeStationModel>.</returns> | |
List<YouBikeStationModel> GetStations(); | |
} | |
} |
YouBikeRepository.cs
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using Newtonsoft.Json; | |
namespace SampleWebApi.Infrastructure.Reposiotry | |
{ | |
/// <summary> | |
/// Class YouBikeRepository. | |
/// </summary> | |
public class YouBikeRepository : IYouBikeRepository | |
{ | |
private Uri DataSource => new Uri("http://data.taipei/youbike"); | |
private TimeSpan DefaultTimeout => new TimeSpan(0, 0, 30); | |
/// <summary> | |
/// 取得所有 YouBike 場站資料. | |
/// </summary> | |
/// <returns>List<YouBikeStationModel>.</returns> | |
public List<YouBikeStationModel> GetStations() | |
{ | |
var originalData = this.RetriveData(); | |
if (originalData == null || originalData.RetuenCode != 1) | |
{ | |
return new List<YouBikeStationModel>(); | |
} | |
var stations = originalData.ReturnValue.PropertyValues() | |
.Where(item => item.HasValues) | |
.Select | |
( | |
item => JsonConvert.DeserializeObject<YouBikeStationModel> | |
( | |
item.ToString() | |
) | |
) | |
.ToList(); | |
return stations; | |
} | |
/// <summary> | |
/// 取得 YouBike 原始 API 資料. | |
/// </summary> | |
/// <returns>List<YouBikeStationModel>.</returns> | |
private YouBikeOriginalModel RetriveData() | |
{ | |
using (HttpClient httpClient = new HttpClient | |
{ | |
Timeout = DefaultTimeout | |
}) | |
{ | |
httpClient.DefaultRequestHeaders.Accept.Add | |
( | |
new MediaTypeWithQualityHeaderValue("application/json") | |
); | |
HttpResponseMessage response = httpClient.GetAsync(DataSource).Result; | |
var responseStream = response.Content.ReadAsStreamAsync().Result; | |
using (GZipStream decompresStream = new GZipStream(responseStream, CompressionMode.Decompress)) | |
using (StreamReader reader = new StreamReader(decompresStream)) | |
{ | |
var jsonSerializer = new JsonSerializer(); | |
var jsonTextReader = new JsonTextReader(reader); | |
var originalData = jsonSerializer.Deserialize<YouBikeOriginalModel>(jsonTextReader); | |
return originalData; | |
} | |
} | |
} | |
} | |
} |
在 YouBikeRepository 的 RetriveData 方法裡,將原本被 gz 壓縮的內容使用 GZipStream 去解壓縮
在 RetriveData 方法最後所取得的是原始 YouBike 場站資料,還沒有對 retVal 做處理,retVal 型別為 JObject,這是為了要做接下來的 JSON 內容轉換而選擇的型別,
在 ReturnValue 屬性 ( JObject ) 裡,將 ChildrenTokens 展開後就可以看到各個的場站資料
最後在 GetStations 方法裡取得 JObject 的 PropertyValues,然後再逐一對取得的 Value 最反序列化為 YouBikeStationModel 處理,如此一來就可以取得全部的 YouBike 場站資料了
Service - 建立 Dto 類別
YouBikeStationDto.cs
using System; | |
namespace SampleWebApi.Infrastructure.Service | |
{ | |
public class YouBikeStationDto | |
{ | |
/// <summary> | |
/// 站點代號 | |
/// </summary> | |
public string No { get; set; } | |
/// <summary> | |
/// 場站名稱(中文) | |
/// </summary> | |
public string Name { get; set; } | |
/// <summary> | |
/// 場站總停車格 | |
/// </summary> | |
public int Total { get; set; } | |
/// <summary> | |
/// 場站目前車輛數量 | |
/// </summary> | |
public int Bikes { get; set; } | |
/// <summary> | |
/// 場站區域(中文) | |
/// </summary> | |
public string Area { get; set; } | |
/// <summary> | |
/// 資料更新時間 | |
/// </summary> | |
public DateTime ModifyTime { get; set; } | |
/// <summary> | |
/// 緯度 | |
/// </summary> | |
public double Latitude { get; set; } | |
/// <summary> | |
/// 經度 | |
/// </summary> | |
public double Longitude { get; set; } | |
/// <summary> | |
/// 地址(中文) | |
/// </summary> | |
public string Address { get; set; } | |
/// <summary> | |
/// 場站區域(英文) | |
/// </summary> | |
public string AreaEnglish { get; set; } | |
/// <summary> | |
/// 場站名稱(英文) | |
/// </summary> | |
public string NameEnglish { get; set; } | |
/// <summary> | |
/// 地址(英文) | |
/// </summary> | |
public string AddressEnglish { get; set; } | |
/// <summary> | |
/// 空位數量 | |
/// </summary> | |
public int BikeEmpty { get; set; } | |
/// <summary> | |
/// 禁用狀態 (false:禁用, true:正常) | |
/// </summary> | |
public bool Active { get; set; } | |
} | |
} |
Service - 建立 Service 類別
IYouBikeService.cs
using System; | |
using System.Collections.Generic; | |
namespace SampleWebApi.Infrastructure.Service | |
{ | |
/// <summary> | |
/// Interface IYouBikeService | |
/// </summary> | |
public interface IYouBikeService | |
{ | |
/// <summary> | |
/// 取得 YouBike 場站資料. | |
/// </summary> | |
/// <returns>List<YouBikeStationDto>.</returns> | |
List<YouBikeStationDto> GetStations(); | |
} | |
} |
YouBikeService.cs
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using AutoMapper; | |
using SampleWebApi.Infrastructure.Reposiotry; | |
namespace SampleWebApi.Infrastructure.Service | |
{ | |
/// <summary> | |
/// Class YouBikeService. | |
/// </summary> | |
/// <seealso cref="SampleWebApi.Infrastructure.Service.IYouBikeService" /> | |
public class YouBikeService : IYouBikeService | |
{ | |
private IMapper Mapper { get; set; } | |
private IYouBikeRepository YouBikeRepository { get; set; } | |
public YouBikeService(IMapper mapper, | |
IYouBikeRepository repository) | |
{ | |
this.Mapper = mapper; | |
this.YouBikeRepository = repository; | |
} | |
/// <summary> | |
/// 取得 YouBike 場站資料. | |
/// </summary> | |
/// <returns>List<YouBikeStationDto>.</returns> | |
public List<YouBikeStationDto> GetStations() | |
{ | |
var sourceData = this.YouBikeRepository.GetStations(); | |
if (sourceData.Any().Equals(false)) | |
{ | |
return new List<YouBikeStationDto>(); | |
} | |
var stations = this.Mapper.Map<List<YouBikeStationModel>, List<YouBikeStationDto>> | |
( | |
sourceData | |
); | |
return stations; | |
} | |
} | |
} |
專案加入 AutoMapper 的 Nuget packages
在 YouBikeService 的 GetStations 方法裡會使用到 AutoMapper 將 YouBikeStationModel 對映轉換為 YouBikeStationDto,所以專案要加入 AutoMapper 的相關 NuGet Packages
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection
Mapping - 新增 AutoMapper 對映轉換設定
Infrastructure 新增 Mapping 資料夾,然後增加 MappingProfile.cs 類別,
記得類別要繼承 AutoMapper 的 Profile,這樣專案啟動後 AutoMapper 才知道專案裡有多少的 Mapper 設定
MappingProfile.cs
using System; | |
using System.Globalization; | |
using AutoMapper; | |
using SampleWebApi.Infrastructure.Reposiotry; | |
using SampleWebApi.Infrastructure.Service; | |
namespace SampleWebApi.Infrastructure.Mapping | |
{ | |
public class MappingProfile : Profile | |
{ | |
const string DateTimeFormat = "yyyyMMddHHmmssfff"; | |
public MappingProfile() | |
{ | |
CreateMap<YouBikeStationModel, YouBikeStationDto>() | |
.ForMember(d => d.No, o => o.MapFrom(s => s.No)) | |
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name)) | |
.ForMember(d => d.Area, o => o.MapFrom(s => s.Area)) | |
.ForMember(d => d.Address, o => o.MapFrom(s => s.Address)) | |
.ForMember(d => d.Total, o => o.MapFrom(s => s.Total)) | |
.ForMember(d => d.Bikes, o => o.MapFrom(s => s.Bikes)) | |
.ForMember(d => d.BikeEmpty, o => o.MapFrom(s => s.BikeEmpty)) | |
.ForMember(d => d.Latitude, o => o.MapFrom(s => s.Latitude)) | |
.ForMember(d => d.Longitude, o => o.MapFrom(s => s.Longitude)) | |
.ForMember(d => d.NameEnglish, o => o.MapFrom(s => s.NameEnglish)) | |
.ForMember(d => d.AreaEnglish, o => o.MapFrom(s => s.AreaEnglish)) | |
.ForMember(d => d.AddressEnglish, o => o.MapFrom(s => s.AddressEnglish)) | |
.ForMember(d => d.Active, o => o.MapFrom(s => s.Active.Equals("1"))) | |
.ForMember | |
( | |
d => d.ModifyTime, | |
o => o.MapFrom | |
( | |
s => string.IsNullOrWhiteSpace(s.ModifyTime) | |
? DateTime.MinValue | |
: DateTime.ParseExact($"{s.ModifyTime}000", DateTimeFormat, CultureInfo.InvariantCulture) | |
) | |
); | |
} | |
} | |
} |
修改 Startup.cs
修改 ConfigureServices 方法,加入 Dependency Injection 設定,包含 AutoMapper, YouBikeRepository, YouBikeService
修改 ValuesController.cs
類別新增型別為 IYouBikeService 的屬性,增加 ValuesController 的建構式並且增加注入 IYouBikeService 的處理,最後 Get 方法裡透過 YouBikeService.GetStations 方法所取得的 YouBike 場站資料輸出
輸出結果
看起來整個功能應該都做完了,不過還是有很多可以加強和修改的地方,例如每次取 API 資料都會再跟資料源頭取一次,但原始的 YouBike 資料是每分鐘更新一次,所以應該要把取得的原始資料做快取,存續時間為一分鐘,然後抓原始資料的處理應該是要放在背景並且使用排程方式處理,而不是由 WebAPI 的 Request 去驅動抓取原始資料。每個 YouBike 場站的資料(排除車輛的可租、可還、更新時間等會變動的內容)其實是應該個別儲存,爾後還可以做另外的延伸應用。
全台灣有使用 YouBike 系統的縣市區域有六個,但並非每個縣市區域都有把場站資料給公開出來讓大家使用,另外格式也不是都一樣,但還是有地方可以取得這些資料,有時間的話就另外再寫一篇文章來說明。
以上
沒有留言:
張貼留言