WebServiceDashboardApi.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.Auth;
  16. using DnsServerCore.Dns;
  17. using Microsoft.AspNetCore.Http;
  18. using System;
  19. using System.Collections.Generic;
  20. using System.Globalization;
  21. using System.Net;
  22. using System.Text.Json;
  23. using System.Threading.Tasks;
  24. using TechnitiumLibrary.Net.Dns;
  25. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  26. namespace DnsServerCore
  27. {
  28. class WebServiceDashboardApi
  29. {
  30. #region variables
  31. readonly DnsWebService _dnsWebService;
  32. #endregion
  33. #region constructor
  34. public WebServiceDashboardApi(DnsWebService dnsWebService)
  35. {
  36. _dnsWebService = dnsWebService;
  37. }
  38. #endregion
  39. #region private
  40. private static void WriteChartDataSet(Utf8JsonWriter jsonWriter, string label, string backgroundColor, string borderColor, List<KeyValuePair<string, long>> statsPerInterval)
  41. {
  42. jsonWriter.WriteStartObject();
  43. jsonWriter.WriteString("label", label);
  44. jsonWriter.WriteString("backgroundColor", backgroundColor);
  45. jsonWriter.WriteString("borderColor", borderColor);
  46. jsonWriter.WriteNumber("borderWidth", 2);
  47. jsonWriter.WriteBoolean("fill", true);
  48. jsonWriter.WritePropertyName("data");
  49. jsonWriter.WriteStartArray();
  50. foreach (KeyValuePair<string, long> item in statsPerInterval)
  51. jsonWriter.WriteNumberValue(item.Value);
  52. jsonWriter.WriteEndArray();
  53. jsonWriter.WriteEndObject();
  54. }
  55. private async Task<IDictionary<string, string>> ResolvePtrTopClientsAsync(List<KeyValuePair<string, long>> topClients)
  56. {
  57. IDictionary<string, string> dhcpClientIpMap = _dnsWebService.DhcpServer.GetAddressHostNameMap();
  58. async Task<KeyValuePair<string, string>> ResolvePtrAsync(string ip)
  59. {
  60. if (dhcpClientIpMap.TryGetValue(ip, out string dhcpDomain))
  61. return new KeyValuePair<string, string>(ip, dhcpDomain);
  62. IPAddress address = IPAddress.Parse(ip);
  63. if (IPAddress.IsLoopback(address))
  64. return new KeyValuePair<string, string>(ip, "localhost");
  65. DnsDatagram ptrResponse = await _dnsWebService.DnsServer.DirectQueryAsync(new DnsQuestionRecord(address, DnsClass.IN), 500);
  66. if (ptrResponse.Answer.Count > 0)
  67. {
  68. IReadOnlyList<string> ptrDomains = DnsClient.ParseResponsePTR(ptrResponse);
  69. if (ptrDomains.Count > 0)
  70. return new KeyValuePair<string, string>(ip, ptrDomains[0]);
  71. }
  72. return new KeyValuePair<string, string>(ip, null);
  73. }
  74. List<Task<KeyValuePair<string, string>>> resolverTasks = new List<Task<KeyValuePair<string, string>>>();
  75. foreach (KeyValuePair<string, long> item in topClients)
  76. {
  77. resolverTasks.Add(ResolvePtrAsync(item.Key));
  78. }
  79. Dictionary<string, string> result = new Dictionary<string, string>();
  80. foreach (Task<KeyValuePair<string, string>> resolverTask in resolverTasks)
  81. {
  82. try
  83. {
  84. KeyValuePair<string, string> ptrResult = await resolverTask;
  85. result[ptrResult.Key] = ptrResult.Value;
  86. }
  87. catch
  88. { }
  89. }
  90. return result;
  91. }
  92. #endregion
  93. #region public
  94. public async Task GetStats(HttpContext context)
  95. {
  96. if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, context.GetCurrentSession().User, PermissionFlag.View))
  97. throw new DnsWebServiceException("Access was denied.");
  98. HttpRequest request = context.Request;
  99. string strType = request.GetQueryOrForm("type", "lastHour");
  100. bool utcFormat = request.GetQueryOrForm("utc", bool.Parse, false);
  101. bool isLanguageEnUs = true;
  102. string acceptLanguage = request.Headers["Accept-Language"];
  103. if (!string.IsNullOrEmpty(acceptLanguage))
  104. isLanguageEnUs = acceptLanguage.StartsWith("en-us", StringComparison.OrdinalIgnoreCase);
  105. Dictionary<string, List<KeyValuePair<string, long>>> data;
  106. string labelFormat;
  107. switch (strType.ToLower())
  108. {
  109. case "lasthour":
  110. data = _dnsWebService.DnsServer.StatsManager.GetLastHourMinuteWiseStats(utcFormat);
  111. labelFormat = "HH:mm";
  112. break;
  113. case "lastday":
  114. data = _dnsWebService.DnsServer.StatsManager.GetLastDayHourWiseStats(utcFormat);
  115. if (isLanguageEnUs)
  116. labelFormat = "MM/DD HH:00";
  117. else
  118. labelFormat = "DD/MM HH:00";
  119. break;
  120. case "lastweek":
  121. data = _dnsWebService.DnsServer.StatsManager.GetLastWeekDayWiseStats(utcFormat);
  122. if (isLanguageEnUs)
  123. labelFormat = "MM/DD";
  124. else
  125. labelFormat = "DD/MM";
  126. break;
  127. case "lastmonth":
  128. data = _dnsWebService.DnsServer.StatsManager.GetLastMonthDayWiseStats(utcFormat);
  129. if (isLanguageEnUs)
  130. labelFormat = "MM/DD";
  131. else
  132. labelFormat = "DD/MM";
  133. break;
  134. case "lastyear":
  135. labelFormat = "MM/YYYY";
  136. data = _dnsWebService.DnsServer.StatsManager.GetLastYearMonthWiseStats(utcFormat);
  137. break;
  138. case "custom":
  139. string strStartDate = request.GetQueryOrForm("start");
  140. string strEndDate = request.GetQueryOrForm("end");
  141. if (!DateTime.TryParse(strStartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime startDate))
  142. throw new DnsWebServiceException("Invalid start date format.");
  143. if (!DateTime.TryParse(strEndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime endDate))
  144. throw new DnsWebServiceException("Invalid end date format.");
  145. if (startDate > endDate)
  146. throw new DnsWebServiceException("Start date must be less than or equal to end date.");
  147. if ((Convert.ToInt32((endDate - startDate).TotalDays) + 1) > 7)
  148. {
  149. data = _dnsWebService.DnsServer.StatsManager.GetDayWiseStats(startDate, endDate, utcFormat);
  150. if (isLanguageEnUs)
  151. labelFormat = "MM/DD";
  152. else
  153. labelFormat = "DD/MM";
  154. }
  155. else
  156. {
  157. data = _dnsWebService.DnsServer.StatsManager.GetHourWiseStats(startDate, endDate, utcFormat);
  158. if (isLanguageEnUs)
  159. labelFormat = "MM/DD HH:00";
  160. else
  161. labelFormat = "DD/MM HH:00";
  162. }
  163. break;
  164. default:
  165. throw new DnsWebServiceException("Unknown stats type requested: " + strType);
  166. }
  167. Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();
  168. //stats
  169. {
  170. List<KeyValuePair<string, long>> stats = data["stats"];
  171. jsonWriter.WritePropertyName("stats");
  172. jsonWriter.WriteStartObject();
  173. foreach (KeyValuePair<string, long> item in stats)
  174. jsonWriter.WriteNumber(item.Key, item.Value);
  175. jsonWriter.WriteNumber("zones", _dnsWebService.DnsServer.AuthZoneManager.TotalZones);
  176. jsonWriter.WriteNumber("cachedEntries", _dnsWebService.DnsServer.CacheZoneManager.TotalEntries);
  177. jsonWriter.WriteNumber("allowedZones", _dnsWebService.DnsServer.AllowedZoneManager.TotalZonesAllowed);
  178. jsonWriter.WriteNumber("blockedZones", _dnsWebService.DnsServer.BlockedZoneManager.TotalZonesBlocked);
  179. jsonWriter.WriteNumber("allowListZones", _dnsWebService.DnsServer.BlockListZoneManager.TotalZonesAllowed);
  180. jsonWriter.WriteNumber("blockListZones", _dnsWebService.DnsServer.BlockListZoneManager.TotalZonesBlocked);
  181. jsonWriter.WriteEndObject();
  182. }
  183. //main chart
  184. {
  185. jsonWriter.WritePropertyName("mainChartData");
  186. jsonWriter.WriteStartObject();
  187. //label format
  188. {
  189. jsonWriter.WriteString("labelFormat", labelFormat);
  190. }
  191. //label
  192. {
  193. List<KeyValuePair<string, long>> statsPerInterval = data["totalQueriesPerInterval"];
  194. jsonWriter.WritePropertyName("labels");
  195. jsonWriter.WriteStartArray();
  196. foreach (KeyValuePair<string, long> item in statsPerInterval)
  197. jsonWriter.WriteStringValue(item.Key);
  198. jsonWriter.WriteEndArray();
  199. }
  200. //datasets
  201. {
  202. jsonWriter.WritePropertyName("datasets");
  203. jsonWriter.WriteStartArray();
  204. WriteChartDataSet(jsonWriter, "Total", "rgba(102, 153, 255, 0.1)", "rgb(102, 153, 255)", data["totalQueriesPerInterval"]);
  205. WriteChartDataSet(jsonWriter, "No Error", "rgba(92, 184, 92, 0.1)", "rgb(92, 184, 92)", data["totalNoErrorPerInterval"]);
  206. WriteChartDataSet(jsonWriter, "Server Failure", "rgba(217, 83, 79, 0.1)", "rgb(217, 83, 79)", data["totalServerFailurePerInterval"]);
  207. WriteChartDataSet(jsonWriter, "NX Domain", "rgba(7, 7, 7, 0.1)", "rgb(7, 7, 7)", data["totalNxDomainPerInterval"]);
  208. WriteChartDataSet(jsonWriter, "Refused", "rgba(91, 192, 222, 0.1)", "rgb(91, 192, 222)", data["totalRefusedPerInterval"]);
  209. WriteChartDataSet(jsonWriter, "Authoritative", "rgba(150, 150, 0, 0.1)", "rgb(150, 150, 0)", data["totalAuthHitPerInterval"]);
  210. WriteChartDataSet(jsonWriter, "Recursive", "rgba(23, 162, 184, 0.1)", "rgb(23, 162, 184)", data["totalRecursionsPerInterval"]);
  211. WriteChartDataSet(jsonWriter, "Cached", "rgba(111, 84, 153, 0.1)", "rgb(111, 84, 153)", data["totalCacheHitPerInterval"]);
  212. WriteChartDataSet(jsonWriter, "Blocked", "rgba(255, 165, 0, 0.1)", "rgb(255, 165, 0)", data["totalBlockedPerInterval"]);
  213. WriteChartDataSet(jsonWriter, "Clients", "rgba(51, 122, 183, 0.1)", "rgb(51, 122, 183)", data["totalClientsPerInterval"]);
  214. jsonWriter.WriteEndArray();
  215. }
  216. jsonWriter.WriteEndObject();
  217. }
  218. //query response chart
  219. {
  220. jsonWriter.WritePropertyName("queryResponseChartData");
  221. jsonWriter.WriteStartObject();
  222. List<KeyValuePair<string, long>> stats = data["stats"];
  223. //labels
  224. {
  225. jsonWriter.WritePropertyName("labels");
  226. jsonWriter.WriteStartArray();
  227. foreach (KeyValuePair<string, long> item in stats)
  228. {
  229. switch (item.Key)
  230. {
  231. case "totalAuthoritative":
  232. jsonWriter.WriteStringValue("Authoritative");
  233. break;
  234. case "totalRecursive":
  235. jsonWriter.WriteStringValue("Recursive");
  236. break;
  237. case "totalCached":
  238. jsonWriter.WriteStringValue("Cached");
  239. break;
  240. case "totalBlocked":
  241. jsonWriter.WriteStringValue("Blocked");
  242. break;
  243. }
  244. }
  245. jsonWriter.WriteEndArray();
  246. }
  247. //datasets
  248. {
  249. jsonWriter.WritePropertyName("datasets");
  250. jsonWriter.WriteStartArray();
  251. jsonWriter.WriteStartObject();
  252. jsonWriter.WritePropertyName("data");
  253. jsonWriter.WriteStartArray();
  254. foreach (KeyValuePair<string, long> item in stats)
  255. {
  256. switch (item.Key)
  257. {
  258. case "totalAuthoritative":
  259. case "totalRecursive":
  260. case "totalCached":
  261. case "totalBlocked":
  262. jsonWriter.WriteNumberValue(item.Value);
  263. break;
  264. }
  265. }
  266. jsonWriter.WriteEndArray();
  267. jsonWriter.WritePropertyName("backgroundColor");
  268. jsonWriter.WriteStartArray();
  269. jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)");
  270. jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)");
  271. jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)");
  272. jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)");
  273. jsonWriter.WriteEndArray();
  274. jsonWriter.WriteEndObject();
  275. jsonWriter.WriteEndArray();
  276. }
  277. jsonWriter.WriteEndObject();
  278. }
  279. //query type chart
  280. {
  281. jsonWriter.WritePropertyName("queryTypeChartData");
  282. jsonWriter.WriteStartObject();
  283. List<KeyValuePair<string, long>> queryTypes = data["queryTypes"];
  284. //labels
  285. {
  286. jsonWriter.WritePropertyName("labels");
  287. jsonWriter.WriteStartArray();
  288. foreach (KeyValuePair<string, long> item in queryTypes)
  289. jsonWriter.WriteStringValue(item.Key);
  290. jsonWriter.WriteEndArray();
  291. }
  292. //datasets
  293. {
  294. jsonWriter.WritePropertyName("datasets");
  295. jsonWriter.WriteStartArray();
  296. jsonWriter.WriteStartObject();
  297. jsonWriter.WritePropertyName("data");
  298. jsonWriter.WriteStartArray();
  299. foreach (KeyValuePair<string, long> item in queryTypes)
  300. jsonWriter.WriteNumberValue(item.Value);
  301. jsonWriter.WriteEndArray();
  302. jsonWriter.WritePropertyName("backgroundColor");
  303. jsonWriter.WriteStartArray();
  304. jsonWriter.WriteStringValue("rgba(102, 153, 255, 0.5)");
  305. jsonWriter.WriteStringValue("rgba(92, 184, 92, 0.5)");
  306. jsonWriter.WriteStringValue("rgba(7, 7, 7, 0.5)");
  307. jsonWriter.WriteStringValue("rgba(91, 192, 222, 0.5)");
  308. jsonWriter.WriteStringValue("rgba(150, 150, 0, 0.5)");
  309. jsonWriter.WriteStringValue("rgba(23, 162, 184, 0.5)");
  310. jsonWriter.WriteStringValue("rgba(111, 84, 153, 0.5)");
  311. jsonWriter.WriteStringValue("rgba(255, 165, 0, 0.5)");
  312. jsonWriter.WriteStringValue("rgba(51, 122, 183, 0.5)");
  313. jsonWriter.WriteStringValue("rgba(150, 150, 150, 0.5)");
  314. jsonWriter.WriteEndArray();
  315. jsonWriter.WriteEndObject();
  316. jsonWriter.WriteEndArray();
  317. }
  318. jsonWriter.WriteEndObject();
  319. }
  320. //top clients
  321. {
  322. List<KeyValuePair<string, long>> topClients = data["topClients"];
  323. IDictionary<string, string> clientIpMap = await ResolvePtrTopClientsAsync(topClients);
  324. jsonWriter.WritePropertyName("topClients");
  325. jsonWriter.WriteStartArray();
  326. foreach (KeyValuePair<string, long> item in topClients)
  327. {
  328. jsonWriter.WriteStartObject();
  329. jsonWriter.WriteString("name", item.Key);
  330. if (clientIpMap.TryGetValue(item.Key, out string clientDomain) && !string.IsNullOrEmpty(clientDomain))
  331. jsonWriter.WriteString("domain", clientDomain);
  332. jsonWriter.WriteNumber("hits", item.Value);
  333. jsonWriter.WriteEndObject();
  334. }
  335. jsonWriter.WriteEndArray();
  336. }
  337. //top domains
  338. {
  339. List<KeyValuePair<string, long>> topDomains = data["topDomains"];
  340. jsonWriter.WritePropertyName("topDomains");
  341. jsonWriter.WriteStartArray();
  342. foreach (KeyValuePair<string, long> item in topDomains)
  343. {
  344. jsonWriter.WriteStartObject();
  345. jsonWriter.WriteString("name", item.Key);
  346. jsonWriter.WriteNumber("hits", item.Value);
  347. jsonWriter.WriteEndObject();
  348. }
  349. jsonWriter.WriteEndArray();
  350. }
  351. //top blocked domains
  352. {
  353. List<KeyValuePair<string, long>> topBlockedDomains = data["topBlockedDomains"];
  354. jsonWriter.WritePropertyName("topBlockedDomains");
  355. jsonWriter.WriteStartArray();
  356. foreach (KeyValuePair<string, long> item in topBlockedDomains)
  357. {
  358. jsonWriter.WriteStartObject();
  359. jsonWriter.WriteString("name", item.Key);
  360. jsonWriter.WriteNumber("hits", item.Value);
  361. jsonWriter.WriteEndObject();
  362. }
  363. jsonWriter.WriteEndArray();
  364. }
  365. }
  366. public async Task GetTopStats(HttpContext context)
  367. {
  368. if (!_dnsWebService._authManager.IsPermitted(PermissionSection.Dashboard, context.GetCurrentSession().User, PermissionFlag.View))
  369. throw new DnsWebServiceException("Access was denied.");
  370. HttpRequest request = context.Request;
  371. string strType = request.GetQueryOrForm("type", "lastHour");
  372. TopStatsType statsType = request.GetQueryOrFormEnum<TopStatsType>("statsType");
  373. int limit = request.GetQueryOrForm("limit", int.Parse, 1000);
  374. List<KeyValuePair<string, long>> topStatsData;
  375. switch (strType.ToLower())
  376. {
  377. case "lasthour":
  378. topStatsData = _dnsWebService.DnsServer.StatsManager.GetLastHourTopStats(statsType, limit);
  379. break;
  380. case "lastday":
  381. topStatsData = _dnsWebService.DnsServer.StatsManager.GetLastDayTopStats(statsType, limit);
  382. break;
  383. case "lastweek":
  384. topStatsData = _dnsWebService.DnsServer.StatsManager.GetLastWeekTopStats(statsType, limit);
  385. break;
  386. case "lastmonth":
  387. topStatsData = _dnsWebService.DnsServer.StatsManager.GetLastMonthTopStats(statsType, limit);
  388. break;
  389. case "lastyear":
  390. topStatsData = _dnsWebService.DnsServer.StatsManager.GetLastYearTopStats(statsType, limit);
  391. break;
  392. case "custom":
  393. string strStartDate = request.GetQueryOrForm("start");
  394. string strEndDate = request.GetQueryOrForm("end");
  395. if (!DateTime.TryParseExact(strStartDate, "yyyy-M-d", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime startDate))
  396. throw new DnsWebServiceException("Invalid start date format.");
  397. if (!DateTime.TryParseExact(strEndDate, "yyyy-M-d", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime endDate))
  398. throw new DnsWebServiceException("Invalid end date format.");
  399. if (startDate > endDate)
  400. throw new DnsWebServiceException("Start date must be less than or equal to end date.");
  401. if ((Convert.ToInt32((endDate - startDate).TotalDays) + 1) > 7)
  402. topStatsData = _dnsWebService.DnsServer.StatsManager.GetDayWiseTopStats(startDate, endDate, statsType, limit);
  403. else
  404. topStatsData = _dnsWebService.DnsServer.StatsManager.GetHourWiseTopStats(startDate, endDate, statsType, limit);
  405. break;
  406. default:
  407. throw new DnsWebServiceException("Unknown stats type requested: " + strType);
  408. }
  409. Utf8JsonWriter jsonWriter = context.GetCurrentJsonWriter();
  410. switch (statsType)
  411. {
  412. case TopStatsType.TopClients:
  413. {
  414. IDictionary<string, string> clientIpMap = await ResolvePtrTopClientsAsync(topStatsData);
  415. jsonWriter.WritePropertyName("topClients");
  416. jsonWriter.WriteStartArray();
  417. foreach (KeyValuePair<string, long> item in topStatsData)
  418. {
  419. jsonWriter.WriteStartObject();
  420. jsonWriter.WriteString("name", item.Key);
  421. if (clientIpMap.TryGetValue(item.Key, out string clientDomain) && !string.IsNullOrEmpty(clientDomain))
  422. jsonWriter.WriteString("domain", clientDomain);
  423. jsonWriter.WriteNumber("hits", item.Value);
  424. jsonWriter.WriteEndObject();
  425. }
  426. jsonWriter.WriteEndArray();
  427. }
  428. break;
  429. case TopStatsType.TopDomains:
  430. {
  431. jsonWriter.WritePropertyName("topDomains");
  432. jsonWriter.WriteStartArray();
  433. foreach (KeyValuePair<string, long> item in topStatsData)
  434. {
  435. jsonWriter.WriteStartObject();
  436. jsonWriter.WriteString("name", item.Key);
  437. jsonWriter.WriteNumber("hits", item.Value);
  438. jsonWriter.WriteEndObject();
  439. }
  440. jsonWriter.WriteEndArray();
  441. }
  442. break;
  443. case TopStatsType.TopBlockedDomains:
  444. {
  445. jsonWriter.WritePropertyName("topBlockedDomains");
  446. jsonWriter.WriteStartArray();
  447. foreach (KeyValuePair<string, long> item in topStatsData)
  448. {
  449. jsonWriter.WriteStartObject();
  450. jsonWriter.WriteString("name", item.Key);
  451. jsonWriter.WriteNumber("hits", item.Value);
  452. jsonWriter.WriteEndObject();
  453. }
  454. jsonWriter.WriteEndArray();
  455. }
  456. break;
  457. default:
  458. throw new NotSupportedException();
  459. }
  460. }
  461. #endregion
  462. }
  463. }