.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现 .net core实践系列之短信服务-架构设计

前言

上篇《.net core实践系列之短信服务-架构设计》介绍了我对短信服务的架构设计,同时针对场景解析了个人设计理念。本篇继续讲解Api服务的实现过程。html

源码地址:https://github.com/SkyChenSky/Sikiro.SMSgit

此服务会使用.NET Core WebApi进行搭建,.NET Core WebApi基础原型就是RESTful风格,然而什么叫RESTful呢。github

REST API简介

REST

Representational State Transfer的缩写,翻译为“表现层状态转化”,是由Roy Thomas Fieding在他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一种架构思想。sql

而他的论文中提出了一个RESTful应用应该具有的几点约束。mongodb

  • 每一个资源都应该有一个惟一的标识
    • 每个对象或资源均可以经过一个惟一的URI进行寻址,URI的结构应该是简单的。
  • 使用标准的方法来更改资源的状态
    • GET、POST、PUT、PATCH、DELETE
  • Request和Response的自描述
  • 资源多重表述
    • URI所访问的每一个资源均可以使用不一样的形式加以表示(XML或JSON)
  • 无状态的服务
    • 不须要保存会话状态(SESSION),资源自己就是自然的状态,是须要被保存的。

RESTful

当某Web服务遵照了REST这些约束条件和原则,那么咱们能够称它设计风格就是 RESTful。数据库

三特色

REST有三大特色:windows

  • 资源(名词)
  • 动做(动词)
  • 表述(超文本)

资源

抽象的说他能够是音频、也能够是视频,更能够是订单。更俗讲其实就是实体,更接近咱们日常说的“类(class)”。另外REST强调资源有惟一的URI。下面有对比api

动做

主要动做:服务器

  •   GET:检索单个资源;
  •   POST:主要是建立资源,可是GET的参数长度受限,所以也能够用在复杂参数的检索资源场景;
  •   PUT:更新资源全部属性,也能够称为替换资源;
  •   PATCH:更新资源部分属性;
  •   DELETE:删除资源;

表述

对于Request与Response的自描述,而表述方式有多种:XML、JSON等,强调HTTP通讯的语义可见性。架构

对比

RPC

SMSApi.com/api/GetSMS

SMSApi.com/api/CreateSMS

传统的接口设计面向过程的,每一个动做有特定的URI。

REST

SMSApi.com/api/SMS  GET

SMSApi.com/api/SMS  POST

REST API每一个资源只有惟一的URI,而资源能够有不一样的动做执行相应的接口

 

RPC的更加倾向于面向过程,而RESTful则以面向对象的思想进行设计。

接口定义

回到咱们的短信服务,以上面的三特色进行出发,SMS不须要由外部服务进行删除、修改资源所以:

资源:SMS

动做:GET、POST

表述方式:咱们约定Request、Response为JSON格式

 /// <summary>
    /// 短信接口
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class SmsController : ControllerBase
    {
        private readonly SmsService _smsService;
        private readonly IBus _bus;

        public SmsController(SmsService smsService, IBus bus)
        {
            _smsService = smsService;
            _bus = bus;
        }

        /// <summary>
        /// 获取一条短信记录
        /// </summary>
        /// <param name="id">主键</param>
        /// <returns></returns>
        [HttpGet("{id}")]
        public ActionResult<SmsModel> Get(string id)
        {
            if (string.IsNullOrEmpty(id))
                return NotFound();

            var smsService = _smsService.Get(id);
            return smsService.Sms;
        }

        /// <summary>
        /// 发送短信
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult Post([FromBody] List<PostModel> model)
        {
            _smsService.Add(model.MapTo<List<PostModel>, List<AddSmsModel>>());

            _smsService.SmsList.Where(a => a.TimeSendDateTime == null)
                .ToList().MapTo<List<SmsModel>, List<SmsQueueModel>>().ForEach(item =>
                {
                    _bus.Publish(item);
                });

            return Ok();
        }

        /// <summary>
        /// 查询短信记录
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost("_search")]
        public ActionResult<List<SmsModel>> Post([FromBody] SearchModel model)
        {
            _smsService.Search(model.MapTo<SearchModel, SearchSmsModel>());

            return _smsService.SmsList;
        }
    }

功能描述

由上可见一共定义了三个接口

  • GET   http://localhost:port/api/sms/id 获取一条短信记录
  • POST http://localhost:port/api/sms 发送短信
  • POST http://localhost:port/api/sms/_search 查询短信记录

获取一条短信记录就很少解析了

查询短信记录

动做我使用了POST,有人会问检索资源不是用GET么?对,可是GET的参数在URL里是受限的,所以在复杂参数的场景下应该选择POST,然而我是模仿elasticsearch的复杂查询时定义,添加多一个节点/_search申明此URI是作查询的。

发送短信

此接口的实现逻辑主要两件事,持久化到MongoDB,过滤出及时发送的短信记录发送到RabbitMQ。

在持久化以前我作了一个分页的动做,咱们提供出去的接口,同一条短信内容支持N个手机号,可是不一样的短信运营商的所支持一次性发送的手机数量是有限的。

开始实现时,我把分页发送写到队列消费服务的发送短信逻辑里,可是这里有个问题,若是分页后部分发送成功,部分发送失败,那么这个聚合究竟以失败仍是成功的状态标示呢?换句话来讲咱们没法保证聚合内的数据一致性。

所以个人作法就是优先在分页成多个文档存储,那么就能够避免从数据库取出后分页致使部分红功、失败。

public void Add(List<AddSmsModel> smsModels)
        {
            DateTime now = DateTime.Now;

            var smsModel = new List<SmsModel>();
            foreach (var sms in smsModels)
            {
                var maxCount = _smsFactory.Create(sms.Type).MaxCount;
                sms.Mobiles = sms.Mobiles.Distinct().ToList();
                var page = GetPageCount(sms.Mobiles.Count, maxCount);

                var index = 0;
                do
                {
                    var toBeSendPhones = sms.Mobiles.Skip(index * maxCount).Take(maxCount).ToList();
                    smsModel.Add(new SmsModel
                    {
                        Content = sms.Content,
                        CreateDateTime = now,
                        Mobiles = toBeSendPhones,
                        TimeSendDateTime = sms.TimeSendDateTime,
                        Type = sms.Type
                    });
                    index++;
                } while (index < page);
            }

            SmsList = smsModel;

            _mongoProxy.BatchAddAsync(SmsList);
        }

项目相关开源框架

EasyNetQ

EasyNetQ.DI.Microsoft

Sikiro.Nosql.Mongo

log4net

Mapster

EasyNetQ

这个开源框架是针对RabbitMQ.Client的封装,隐藏了不少实现细节,简化使用方式。并提供了多种IOC注入方式

源码地址:https://github.com/EasyNetQ/EasyNetQ

Sikiro.Nosql.Mongo

这个是我本身针对mongo驱动的经常使用的基础操做的封装库

源码地址:https://github.com/SkyChenSky/Sikiro.Nosql.Mongo

Mapster

实体映射框架,看评测数据比AutoMapper等之类的效率要高,并且易用性也很是高。

https://github.com/MapsterMapper/Mapster

全局异常日志记录

public class GolbalExceptionAttribute : ExceptionFilterAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            if (!context.ExceptionHandled)
            {
                context.Exception.WriteToFile();
            }

            base.OnException(context);
        }
    }

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(option =>
                {
                    option.Filters.Add<GolbalExceptionAttribute>();
                })
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

LoggerHelper

上面的WriteToFile是我对Exception的扩展方法,使用了Log4Net日志框架对异常进行记录,若是有须要也能够写到mongodb或者elasticsearch

/// <summary>
    /// 日志帮助类
    /// </summary>
    public static class LoggerHelper
    {
        private static readonly ILoggerRepository Repository = LogManager.CreateRepository("NETCoreRepository");
        public static readonly ILog Log = LogManager.GetLogger(Repository.Name, typeof(LoggerHelper));

        static LoggerHelper()
        {
            XmlConfigurator.Configure(Repository, new FileInfo("log4net.config"));
        }

        #region 文本日志

        /// <summary>
        /// 文本日志
        /// </summary>
        /// <param name="message"></param>
        /// <param name="ex"></param>
        public static void WriteToFile(this Exception ex, string message = null)
        {
            if (string.IsNullOrEmpty(message))
                message = ex.Message;

            Log.Error(message, ex);
        }
        #endregion
    }

工具库的封装

框架与工具库都是以库的形式提供咱们使用,并且都是可复用,可是他们区别在于:工具库开箱即用,大多数以静态方法提供调用,只调用少许甚至一个方法则完成使用。

而框架定义,为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品,而他具备约束性、可复用性、规范性。他是一个半成品,可重写。

所以为了简化框架的使用,对经常使用设置、构建组合进行封装,以一个扩展类或者帮助类的形式提供,简化使用、增长可读性。

Swagger的使用

Http协议的好处是轻量、跨平台,如此良好的灵活性然而须要接口描述对外暴露。Swagger是一个很好的选择,不须要本身手写文档并提供后台管理界面,还能够测试,简化很多工做。

我选择了NSwag.AspNetCore开源组件,他的使用很是简单。只须要两步:

1.配置Swagger:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseSwaggerUiWithApiExplorer(settings =>
           {
               settings.GeneratorSettings.DefaultPropertyNameHandling =
                   PropertyNameHandling.CamelCase;
               settings.PostProcess = document =>
               {
                   document.Info.Version = "v1";
                   document.Info.Title = "Sikiro.SMS.API";
                   document.Info.Description = "短信服务API";
                   document.Info.TermsOfService = "None";
               };
           });
            app.UseMvc();
        }

2.设置站点项目

此设置为了把接口、参数注释显示到Swagger页面

NSwag还有多个版本的UI选择:

  • UseSwaggerReDoc
  • UseSwaggerUi
  • UseSwaggerUi3

访问http://localhost:port/swagger就能够见到API文档了

部署

 由于我公司仍是使用windows server 2008。所以部署前应准备环境安装包:

.NET Core 2.1.3 windows-hosting

安装完成后重启服务器,再把文件发布到服务器,编辑应用程序池为无托管代码。就能够访问了

 

结尾

本篇介绍Sikiro.SMS.Api的设计与实现,下篇会针对API调用进行封装SDK。若是有任何建议,请在下方评论反馈给我。

相关文章
相关标签/搜索