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

使用 .NET Core 构建服务监控应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (38投票s)

2017年1月17日

CPOL

7分钟阅读

viewsIcon

80012

使用 .NET Core 构建服务监控应用程序

引言

本指南将介绍如何创建一个服务监控应用程序,那么它是什么呢?简单来说:它是一个应用程序,允许监控网络中的服务并将监控结果保存在数据库中,这里我们使用 SQL Server。

我知道有很多工具可以提供此功能,也有很多花钱就能买到的更好用的工具,但本指南的目的是展示如何利用 .NET Core 的强大功能构建一个开发者可以根据自定义需求进行扩展的应用程序。

基本思路是这样的:有一个进程可以无限次运行来监控主机、数据库和 API;将监控结果保存在 SQL Server 数据库中,然后我们可以为最终用户构建一个精美的 UI,显示每个服务的状态,我们可以监控很多目标,但最好是允许用户订阅特定的服务而不是全部;例如,**DBA** 需要监控数据库服务器而不是 API,**开发人员** 需要监控开发数据库和 API 等。

还可以设想在您的开发室里放上大屏幕,实时查看您服务的状态,甚至最好能有图表。:)

一个特殊功能是,当一个或多个服务失败时,可以有一个通知服务向所有管理员发送消息,在这种情况下,“服务”意味着一个目标,例如主机、数据库、API。

在本指南中,我们将监控以下服务:

名称 描述
宿主 Ping 一个现有主机
数据库 打开和关闭现有数据库的连接
RESTful API 调用一个现有 API 的操作

背景

正如我们之前所说,我们将创建一个应用程序来监控现有的目标(主机、数据库、API),所以我们需要对这些概念有基本的了解。

我们将通过 ping 操作来监控主机,所以我们会添加与网络相关的包来执行此操作。

我们将通过打开和关闭连接来监控数据库,不要使用集成身份验证,因为您需要用您的凭据来模拟您的服务监控进程,在这种情况下,最好有一个特定的用户来连接数据库,并且只执行此操作以避免被黑客攻击。

RESTful API 将通过 REST 客户端来监控,以调用一个返回简单 JSON 的操作。

数据库

在存储库中,有一个名为 _\Resources\Database_ 的目录,该目录包含相关的数据库文件,请确保按照以下顺序运行这些文件:

文件名 描述
00 - _Database.sql_ 数据库定义
01 - _Tables.sql_ 表定义
02 - _Constraints.sql_ 约束(主键、外键和唯一键)
03 - _Rows.sql_ 初始数据

您可以在 此处 找到数据库脚本。

表描述
表格 描述
EnvironmentCategory 包含所有环境类别:开发、QA 和生产
ServiceCategory 包含所有服务类别:数据库、REST API、服务器、URL 和 Web 服务
Service 包含所有服务定义
ServiceWatcher 包含 C# 端用于执行监控操作的所有组件
ServiceEnvironment 包含服务与环境的关系,例如,我们可以定义一个名为 FinanceService 的服务,并具有不同的环境:开发、QA 和生产
ServiceEnvironmentStatus 包含每个环境中每个服务的状态
ServiceEnvironmentStatusLog 包含每个服务环境状态的详细信息
Owner 包含应用程序的用户列表,代表所有所有者
ServiceOwner 包含服务与所有者的关系
用户 包含所有监控服务用户
ServiceUser 包含服务与用户的关系
请不要忘记我们正在使用一个在本地机器上运行的解决方案,在资源目录中有一个示例 API 用于执行测试,但您需要更改连接字符串并根据您的实际情况添加您的服务。
同样,我不建议在 ServiceEnvironment 表中公开真实的连接字符串,请向您的 DBA 请求一个只能执行打开目标数据库连接的单个用户,如果数据库安全是您负责的任务,请创建仅执行打开数据库连接操作的特定用户,并避免暴露敏感信息。

.NET Core 解决方案

现在我们需要定义此解决方案的项目,以便清晰地了解项目的范围:

项目名称 类型 描述
ServiceMonitor.Core 类库 包含与数据库存储相关的所有定义
ServiceMonitor.Common 类库 包含 ServiceMonitor 项目的通用定义,例如 Watchers、Serializer 和 Clients (REST)
ServiceMonitor.WebAPI Web API 包含用于读写监控信息的 Web API 控制器
ServiceMonitor 控制台应用程序 包含监控所有服务的进程

ServiceMonitor.Core

此项目包含实体和数据库访问的所有定义,因此我们需要为项目添加以下包:

名称 版本 描述
Microsoft.EntityFrameworkCore.SqlServer 最新版本 通过 EF Core 提供对 SQL Server 的访问

此项目包含三个层:业务逻辑、数据库访问和实体;请查看文章 _EF Core for Entreprise_ 以更好地理解此项目和这些层。

DashboardService 类代码

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using ServiceMonitor.Core.BusinessLayer.Contracts;
using ServiceMonitor.Core.BusinessLayer.Responses;
using ServiceMonitor.Core.DataLayer;
using ServiceMonitor.Core.DataLayer.DataContracts;
using ServiceMonitor.Core.EntityLayer;

namespace ServiceMonitor.Core.BusinessLayer
{
    public class DashboardService : Service, IDashboardService
    {
        public DashboardService(ILogger<DashboardService> logger, ServiceMonitorDbContext dbContext)
            : base(logger, dbContext)
        {
        }

        public async Task<IListResponse<ServiceWatcherItemDto>> GetActiveServiceWatcherItemsAsync()
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetActiveServiceWatcherItemsAsync));

            var response = new ListResponse<ServiceWatcherItemDto>();

            try
            {
                response.Model = await DbContext.GetActiveServiceWatcherItems().ToListAsync();

                Logger?.LogInformation("The service watch items were loaded successfully");
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetActiveServiceWatcherItemsAsync), ex);
            }

            return response;
        }

        public async Task<IListResponse<ServiceStatusDetailDto>> GetServiceStatusesAsync(string userName)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusesAsync));

            var response = new ListResponse<ServiceStatusDetailDto>();

            try
            {
                var user = await DbContext.GetUserAsync(userName);

                if (user == null)
                {
                    Logger?.LogInformation("There isn't data for user '{0}'", userName);

                    return new ListResponse<ServiceStatusDetailDto>();
                }
                else
                {
                    response.Model = await DbContext.GetServiceStatuses(user).ToListAsync();

                    Logger?.LogInformation("The service status details for '{0}' user were loaded successfully", userName);
                }
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetServiceStatusesAsync), ex);
            }

            return response;
        }

        public async Task<ISingleResponse<ServiceEnvironmentStatus>> GetServiceStatusAsync(ServiceEnvironmentStatus entity)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceStatusAsync));

            var response = new SingleResponse<ServiceEnvironmentStatus>();

            try
            {
                response.Model = await DbContext.GetServiceEnvironmentStatusAsync(entity);
            }
            catch (Exception ex)
            {
                response.SetError(Logger, nameof(GetServiceStatusAsync), ex);
            }

            return response;
        }
    }
}

ServiceMonitor.Common

合同

  • IWatcher
  • IWatchResponse
  • ISerializer

IWatcher 接口代码

using System.Threading.Tasks;

namespace ServiceMonitor.Common.Contracts
{
    public interface IWatcher
    {
        string ActionName { get; }

        Task<WatchResponse> WatchAsync(WatcherParameter parameter);
    }
}

IWatchResponse 接口代码

namespace ServiceMonitor.Common.Contracts
{
    public interface IWatchResponse
    {
        bool Success { get; set; }

        string Message { get; set; }

        string StackTrace { get; set; }
    }
}

ISerializer 接口代码

namespace ServiceMonitor.Common.Contracts
{
    public interface ISerializer
    {
        string Serialize<T>(T obj);

        T Deserialze<T>(string source);
    }
}

Watchers

这些是实现

  • DatabaseWatcher
  • HttpRequestWatcher
  • PingWatcher

DatabaseWatcher 类代码

using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor.Common
{
    public class DatabaseWatcher : IWatcher
    {
        public string ActionName
            => "OpenDatabaseConnection";

        public async Task<WatchResponse> WatchAsync(WatcherParameter parameter)
        {
            var response = new WatchResponse();

            using (var connection = new SqlConnection(parameter.Values["ConnectionString"]))
            {
                try
                {
                    await connection.OpenAsync();

                    response.Success = true;
                }
                catch (Exception ex)
                {
                    response.Success = false;
                    response.Message = ex.Message;
                    response.StackTrace = ex.ToString();
                }
            }

            return response;
        }
    }
}

HttpWebRequestWatcher 类代码

using System;
using System.Threading.Tasks;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor.Common
{
    public class HttpRequestWatcher : IWatcher
    {
        public string ActionName
            => "HttpRequest";

        public async Task<WatchResponse> WatchAsync(WatcherParameter parameter)
        {
            var response = new WatchResponse();

            try
            {
                var restClient = new RestClient();

                await restClient.GetAsync(parameter.Values["Url"]);

                response.Success = true;
            }
            catch (Exception ex)
            {
                response.Success = false;
                response.Message = ex.Message;
                response.StackTrace = ex.ToString();
            }

            return response;
        }
    }
}

PingWatcher 类代码

using System.Net.NetworkInformation;
using System.Threading.Tasks;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor.Common
{
    public class PingWatcher : IWatcher
    {
        public string ActionName
            => "Ping";

        public async Task<WatchResponse> WatchAsync(WatcherParameter parameter)
        {
            var ping = new Ping();

            var reply = await ping.SendPingAsync(parameter.Values["Address"]);

            return new WatchResponse
            {
                Success = reply.Status == IPStatus.Success ? true : false
            };
        }
    }
}

ServiceMonitor.WebAPI

此项目代表服务监控的 RESTful API,因此我们将有两个控制器:DashboardControllerAdministrationController。Dashboard 包含与最终用户结果相关的所有操作,而 Administration 包含与保存信息(创建、编辑和删除)相关的所有操作。

仪表板

DashboardController 类代码

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using ServiceMonitor.Core.BusinessLayer.Contracts;
using ServiceMonitor.WebAPI.Responses;

namespace ServiceMonitor.WebAPI.Controllers
{
#pragma warning disable CS1591
    [Route("api/v1/[controller]")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        protected readonly ILogger Logger;
        protected readonly IDashboardService Service;

        public DashboardController(ILogger<DashboardController> logger, IDashboardService service)
        {
            Logger = logger;
            Service = service;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Gets service watcher items (registered services to watch with service monitor)
        /// </summary>
        /// <returns>A sequence of services to watch</returns>
        /// <response code="200"></response>
        /// <response code="500"></response>
        [HttpGet("ServiceWatcherItem")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetServiceWatcherItemsAsync()
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceWatcherItemsAsync));

            var response = await Service.GetActiveServiceWatcherItemsAsync();

            return response.ToHttpResponse();
        }
    }
}

Administration

AdministrationController 类代码

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using ServiceMonitor.Core.BusinessLayer.Contracts;
using ServiceMonitor.WebAPI.Responses;

namespace ServiceMonitor.WebAPI.Controllers
{
#pragma warning disable CS1591
    [Route("api/v1/[controller]")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        protected readonly ILogger Logger;
        protected readonly IDashboardService Service;

        public DashboardController(ILogger<DashboardController> logger, IDashboardService service)
        {
            Logger = logger;
            Service = service;
        }
#pragma warning restore CS1591

        /// <summary>
        /// Gets service watcher items (registered services to watch with service monitor)
        /// </summary>
        /// <returns>A sequence of services to watch</returns>
        /// <response code="200"></response>
        /// <response code="500"></response>
        [HttpGet("ServiceWatcherItem")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]
        public async Task<IActionResult> GetServiceWatcherItemsAsync()
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetServiceWatcherItemsAsync));

            var response = await Service.GetActiveServiceWatcherItemsAsync();

            return response.ToHttpResponse();
        }
    }
}

ServiceMonitor

此项目包含 Service Monitor Client 的所有对象,在此项目中,我们添加了 Newtonsoft.Json 包用于 JSON 序列化,在 ServiceMonitor.Common 中有一个名为 ISerializer 的接口,这是因为我不想强制使用特定的序列化器,您可以在此级别进行更改。:)

ServiceMonitorSerializer 类代码

using Newtonsoft.Json;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor
{
    public class ServiceMonitorSerializer : ISerializer
    {
        public string Serialize<T>(T obj)
            => JsonConvert.SerializeObject(obj);

        public T Deserialze<T>(string source)
            => JsonConvert.DeserializeObject<T>(source);
    }
}

接下来,我们将处理 MonitorController 类,在这个类中,我们将通过 Service Monitor API 中的 AdministrationController 来执行所有监控操作并将所有结果保存到数据库。

MonitorController 类代码

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ServiceMonitor.Clients;
using ServiceMonitor.Clients.Models;
using ServiceMonitor.Common;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor
{
    public class MonitorController
    {
        public MonitorController(ILogger logger, IWatcher watcher, IServiceMonitorWebAPIClient client, AppSettings appSettings)
        {
            Logger = logger;
            Watcher = watcher;
            Client = client;
            AppSettings = appSettings;
        }

        public ILogger Logger { get; }

        public IWatcher Watcher { get; }

        public IServiceMonitorWebAPIClient Client { get; }

        public AppSettings AppSettings { get; }

        public async Task ProcessAsync(ServiceWatchItem item)
        {
            while (true)
            {
                try
                {
                    Logger?.LogTrace("{0} - Watching '{1}' for '{2}' environment", DateTime.Now, item.ServiceName, item.Environment);

                    var watchResponse = await Watcher.WatchAsync(new WatcherParameter(item.ToDictionary()));

                    if (watchResponse.Success)
                        Logger?.LogInformation(" Success watch for '{0}' in '{1}' environment", item.ServiceName, item.Environment);
                    else
                        Logger?.LogError(" Failed watch for '{0}' in '{1}' environment", item.ServiceName, item.Environment);

                    var serviceStatusLog = new ServiceStatusLogRequest
                    {
                        ServiceID = item.ServiceID,
                        ServiceEnvironmentID = item.ServiceEnvironmentID,
                        Target = item.ServiceName,
                        ActionName = Watcher.ActionName,
                        Success = watchResponse.Success,
                        Message = watchResponse.Message,
                        StackTrace = watchResponse.StackTrace
                    };

                    try
                    {
                        await Client.PostServiceEnvironmentStatusLog(serviceStatusLog);
                    }
                    catch (Exception ex)
                    {
                        Logger?.LogCritical(" Error on saving watch response ({0}): '{1}'", item.ServiceName, ex.Message);
                    }
                }
                catch (Exception ex)
                {
                    Logger?.LogCritical(" Error watching service: '{0}': '{1}'", item.ServiceName, ex.Message);
                }

                Thread.Sleep(item.Interval ?? AppSettings.DelayTime);
            }
        }
    }
}

在运行控制台应用程序之前,请确保以下几点:

  1. ServiceMonitor 数据库可用
  2. ServiceMonitor 数据库包含服务类别、服务、服务监控器和用户的信息
  3. ServiceMonitor API 可用

我们可以检查 url api/v1/Dashboard/ServiceWatcherItems 返回的值

{  
  "message":null,
  "didError":false,
  "errorMessage":null,
  "model":[  
    {  
      "serviceID":1,
      "serviceEnvironmentID":1,
      "environment":"Development",
      "serviceName":"Northwind Database",
      "interval":15000,
      "url":null,
      "address":null,
      "connectionString":"server=(local);database=Northwind;user id=johnd;password=SqlServer2017$",
      "typeName":"ServiceMonitor.Common.DatabaseWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    },
    {  
      "serviceID":2,
      "serviceEnvironmentID":3,
      "environment":"Development",
      "serviceName":"DNS",
      "interval":3000,
      "url":null,
      "address":"192.168.1.1",
      "connectionString":null,
      "typeName":"ServiceMonitor.Common.PingWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    },
    {  
      "serviceID":3,
      "serviceEnvironmentID":4,
      "environment":"Development",
      "serviceName":"Sample API",
      "interval":5000,
      "url":"https://:5612/api/values",
      "address":null,
      "connectionString":null,
      "typeName":"ServiceMonitor.Common.HttpWebRequestWatcher, ServiceMonitor.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    }
  ]
}

正如我们所见,API 返回了 DefaultUser 的所有服务,请记住,一个用户可以订阅多个要监控的服务,显然在这个示例中,我们的默认用户订阅了所有服务,但我们可以更改 ServiceUser 表中的此链接。

Program 类代码

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using ServiceMonitor.Clients;
using ServiceMonitor.Clients.Models;
using ServiceMonitor.Common;
using ServiceMonitor.Common.Contracts;

namespace ServiceMonitor
{
    class Program
    {
        private static ILogger Logger;
        private static readonly AppSettings AppSettings;

        static Program()
        {
            Logger = LoggingHelper.GetLogger<Program>();

            var builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");

            var configuration = builder.Build();

            AppSettings = new AppSettings();

            configuration.GetSection("appSettings").Bind(AppSettings);
        }

        static void Main(string[] args)
        {
            StartAsync(args).GetAwaiter().GetResult();

            Console.ReadLine();
        }

        static async Task StartAsync(string[] args)
        {
            Logger.LogDebug("Starting service monitor...");

            var client = new ServiceMonitorWebAPIClient();

            var serviceWatcherItemsResponse = default(ServiceWatchResponse);

            try
            {
                serviceWatcherItemsResponse = await client.GetServiceWatcherItemsAsync();
            }
            catch (Exception ex)
            {
                Logger.LogError("Error on retrieve watch items: {0}", ex);
                return;
            }

            foreach (var item in serviceWatcherItemsResponse.Model)
            {
                var watcherType = Type.GetType(item.TypeName, true);

                var watcherInstance = Activator.CreateInstance(watcherType) as IWatcher;

                await Task.Factory.StartNew(async () =>
                {
                    var controller = new MonitorController(Logger, watcherInstance, client, AppSettings);

                    await controller.ProcessAsync(item);
                });
            }
        }
    }
}

在检查完上述几点后,现在我们继续运行控制台应用程序,控制台输出如下:

dbug: ServiceMonitor.Program[0]
      Starting application
sr trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:30 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:30 - Watching 'Northwind Database' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:30 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:35 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:37 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:39 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:42 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:43 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:45 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:47 - Watching 'Northwind Database' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:48 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:48 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:51 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:53 - Watching 'Sample API' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:54 - Watching 'DNS' for 'Development' environment
trce: ServiceMonitor.Program[0]
      06/20/2017 23:09:57 - Watching 'DNS' for 'Development' environment

现在我们继续检查数据库中保存的数据,请检查 ServiceEnvironmentStatus 表,您将看到类似这样的结果:

ServiceEnvironmentStatusID ServiceEnvironmentID Success WatchCount  LastWatch
-------------------------- -------------------- ------- ----------- -----------------------
1                          4                    1       212         2018-11-22 23:11:34.113
2                          1                    1       78          2018-11-22 23:11:33.370
3                          3                    1       366         2018-11-22 23:11:34.620

(3 row(s) affected)

这一切是如何协同工作的? 控制台应用程序从 API 获取所有要监控的服务,然后在 MonitorController 中为每个监控项启动一个无限循环任务,每个任务都有一个延迟时间,该间隔在服务定义中设置,但如果未定义间隔值,则从 AppSettings 中获取间隔;因此,在执行 Watch 操作后,结果通过 API 保存到数据库,然后过程重复。如果您想对其他类型执行 watch 操作,您可以创建自己的 Watcher 类。

关注点

  • DatabaseWatcher 使用 SQL Server,那么如何连接到 MySQL、PostgreSQL、Oracle 以及其他 DBMS 呢?创建用于特定 DBMS 的 Watcher 类,实现 IWatcher 接口,并编写连接到目标数据库的代码。
  • 我们可以在非 Windows 平台托管服务监控吗?是的,由于 .NET Core 是跨平台的,我们可以将此项目托管在 Windows、Mac OS 和 Linux 上。
  • 据我所知,.NET Core 中没有对 ASMX 的原生支持,但我们可以监控这两种服务,只需在 Service 表中添加行即可,ASMX 以 _*.asmx*_ 结尾。
  • 为什么控制台客户端和 API 不是一个单一的项目?为了避免发布时出现常见问题,我认为有两个不同的项目更好,因为这样我们就可以在一个服务器上运行服务监控,而在另一个服务器上托管 API。
  • 在这个初始版本中,没有任何关于安全性的配置,因为最好根据您的场景添加该实现;您可以使其与 Windows 身份验证、自定义身份验证一起工作,或者添加外部服务进行身份验证,**这有意义吗?**

代码改进

  • 添加身份服务器
  • 在服务监控过程中发生严重错误时,添加对管理员的通知(电子邮件、短信等)
  • 我认为在 ServiceCategory 中使用 TypeName 比使用 ServiceWatcher 更好
  • 添加 UI 项目,以便以美观的方式向最终用户显示服务状态,使用一些前端框架,例如 Angular

您可以在 Projects 部分查看正在进行中的改进。

历史

  • 2017 年 1 月 17 日:初始版本
  • 2017 年 6 月 20 日:为服务添加环境
  • 2017 年 10 月 19 日:更新服务(业务层)
  • 2018 年 7 月 15 日:更新至 .NET Core 2
  • 2018 年 11 月 22 日:移除存储库模式
  • 2019 年 1 月 10 日:为 Web API 添加帮助页
  • 2019 年 1 月 27 日:代码重构
© . All rights reserved.