.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现
前言
上篇《.net core实践系列之短信服务-架构设计》介绍了我对短信服务的架构设计,同时针对场景解析了我的设计理念。本篇继续讲解Api服务的实现过程。
源码地址:https://github.com/SkyChenSky/Sikiro.SMS
此服务会使用.NET Core WebApi进行搭建,.NET Core WebApi基础原型就是RESTful风格,然而什么叫RESTful呢。
REST API简介
REST
Representational State Transfer的缩写,翻译为“表现层状态转化”,是由Roy Thomas Fieding在他的博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一种架构思想。
而他的论文中提出了一个RESTful应用应该具备的几点约束。
- 每个资源都应该有一个唯一的标识
- 每一个对象或资源都可以通过一个唯一的URI进行寻址,URI的结构应该是简单的。
- 使用标准的方法来更改资源的状态
- GET、POST、PUT、PATCH、DELETE
- Request和Response的自描述
- 资源多重表述
- URI所访问的每个资源都可以使用不同的形式加以表示(XML或JSON)
- 无状态的服务
- 不需要保存会话状态(SESSION),资源本身就是天然的状态,是需要被保存的。
RESTful
当某Web服务遵守了REST这些约束条件和原则,那么我们可以称它设计风格就是 RESTful。
三特点
REST有三大特点:
- 资源(名词)
- 动作(动词)
- 表述(超文本)

资源
抽象的说他可以是音频、也可以是视频,更可以是订单。更俗讲其实就是实体,更接近我们平常说的“类(class)”。另外REST强调资源有唯一的URI。下面有对比
动作
主要动作:
- 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 = ;
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。如果有任何建议,请在下方评论反馈给我。
.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现的更多相关文章
- .net core实践系列之短信服务-目录
前言 经过两周多的业余时间,终于把该系列的文章写完了.第一次写系列,可能部分关键点并没有覆盖到,如果有疑问的朋友可以随时反馈给我.另外也感谢在我发布文章时给予我方案建议与反馈源码BUG的朋友们.下面是 ...
- .net core实践系列之短信服务-架构设计
前言 上篇<.net core实践系列之短信服务-为什么选择.net core(开篇)>简单的介绍了(水了一篇).net core.这次针对短信服务的架构设计和技术栈的简析. 源码地址:h ...
- .net core实践系列之短信服务-Api的SDK的实现与测试
前言 上一篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>讲解了API的设计与实现,本篇主要讲解编写接口的SDK编写还有API的测试. 或许有些人会认为, ...
- .net core实践系列之短信服务-Sikiro.SMS.Bus服务的实现
前言 前两篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>.<.net core实践系列之短信服务-Api的SDK的实现与测试>分别讲解了AP ...
- .net core实践系列之短信服务-架构优化
前言 通过前面的几篇文章,讲解了一个短信服务的架构设计与实现.然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整. 同时我也希望通过这篇文章与大家分享一下,我的架构设计理念. 源码地址 ...
- .net core实践系列之短信服务-Sikiro.SMS.Job服务的实现
前言 本篇会继续讲解Sikiro.SMS.Job服务的实现,在我写第一篇的时候,我就发现我当时设计的架构里Sikiro.SMS.Job这个可以选择不需要,而使用MQ代替.但是为了说明调度任务使用实现也 ...
- .net core实践系列之短信服务-为什么选择.net core(开篇)
前言 从今天我将会写.net core实战系列,以我最近完成的短信服务作为例子.该系列将会尽量以最短的时间全部发布出来.源码也将优先开源出来给大家. 源码地址:https://github.com/S ...
- .net core实践系列之SSO-跨域实现
前言 接着上篇的<.net core实践系列之SSO-同域实现>,这次来聊聊SSO跨域的实现方式.这次虽说是.net core实践,但是核心点使用jquery居多. 建议看这篇文章的朋友可 ...
- .net core 使用阿里云短信发送SMS
阿里云官方的skd(aliyun-net-sdk-core,aliyun-net-sdk-dysmsapi)在dnc中发送短信会出错,nuget上的包貌似也一样不管用.直接改下sdk当然也可以,但就发 ...
随机推荐
- 记一次nginx php配置的心路历程
1.本来搞好了php的配置,想把目录下移一层 从 www.abc.com 变成 www.abc.com/wxapi ,由于我的真实文件目录比路由少了一层public 尝试了很多办法都不行 甚至想到了u ...
- 逻辑回归&线性回归
# coding:utf-8 import numpy as np from sklearn import linear_model, datasets import matplotlib.pyplo ...
- 在 Android 手机上运行 Python 程序
- JQuery 获取多个select标签option的text内容
根据option的id属性,修改text值 $("#sel_div .select_class option[id='-选择省-']").text(data.province).a ...
- 【2017下集美大学软工1412班_助教博客】团队作业7——第二次项目冲刺(Beta阶段)成绩公示
作业要求 团队作业7 团队评分结果 Beta计划 Total GN SX GJ LC AP WT PHILOSOPHER 3 1 0 1.5 0 0.5 0 三人行 3.5 1 1 1 0 0.5 0 ...
- Hibernate Tools生成注释
原文:http://www.blogjava.net/pauliz/archive/2009/11/13/302162.html 有同学需要修改后的Hibernate Tools整个tool我就不上传 ...
- Linux下Sublime Text 3的安装
1.下载 官网下载 或者直接 #wget http://c758482.r82.cf2.rackcdn.com/sublime_text_3_build_3059_x32.tar.bz2 (linux ...
- Django之ORM查询进阶
基于双下划线的双表查询 分组与聚合函数 基于双下划线的双表查询 Django 还提供了一种直观而高效的方式在查询(lookups)中表示关联关系,它能自动确认 SQL JOIN 联系.要做跨关系查询, ...
- 关于Hamilton问题的研究
关于Hamilton问题的研究 首先介绍一下Hamilton问题:哈密顿问题寻找一条从给定的起点到给定的终点沿途恰好经过所有其他结点一次的路径.(摘自百度百科) 从刚开始学OI买了信息学一本通,这个问 ...
- Yii2.0页面提示消息
适用情况:比如提交一个表单,提交完成之后在页面展示一条提示消息. 控制器里面这样写: 单条消息: \Yii::$app->getSession()->setFlash('error', ' ...