前言

上篇《.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服务的实现的更多相关文章

  1. .net core实践系列之短信服务-目录

    前言 经过两周多的业余时间,终于把该系列的文章写完了.第一次写系列,可能部分关键点并没有覆盖到,如果有疑问的朋友可以随时反馈给我.另外也感谢在我发布文章时给予我方案建议与反馈源码BUG的朋友们.下面是 ...

  2. .net core实践系列之短信服务-架构设计

    前言 上篇<.net core实践系列之短信服务-为什么选择.net core(开篇)>简单的介绍了(水了一篇).net core.这次针对短信服务的架构设计和技术栈的简析. 源码地址:h ...

  3. .net core实践系列之短信服务-Api的SDK的实现与测试

    前言 上一篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>讲解了API的设计与实现,本篇主要讲解编写接口的SDK编写还有API的测试. 或许有些人会认为, ...

  4. .net core实践系列之短信服务-Sikiro.SMS.Bus服务的实现

    前言 前两篇<.net core实践系列之短信服务-Sikiro.SMS.Api服务的实现>.<.net core实践系列之短信服务-Api的SDK的实现与测试>分别讲解了AP ...

  5. .net core实践系列之短信服务-架构优化

    前言 通过前面的几篇文章,讲解了一个短信服务的架构设计与实现.然而初始方案并非100%完美的,我们仍可以对该架构做一些优化与调整. 同时我也希望通过这篇文章与大家分享一下,我的架构设计理念. 源码地址 ...

  6. .net core实践系列之短信服务-Sikiro.SMS.Job服务的实现

    前言 本篇会继续讲解Sikiro.SMS.Job服务的实现,在我写第一篇的时候,我就发现我当时设计的架构里Sikiro.SMS.Job这个可以选择不需要,而使用MQ代替.但是为了说明调度任务使用实现也 ...

  7. .net core实践系列之短信服务-为什么选择.net core(开篇)

    前言 从今天我将会写.net core实战系列,以我最近完成的短信服务作为例子.该系列将会尽量以最短的时间全部发布出来.源码也将优先开源出来给大家. 源码地址:https://github.com/S ...

  8. .net core实践系列之SSO-跨域实现

    前言 接着上篇的<.net core实践系列之SSO-同域实现>,这次来聊聊SSO跨域的实现方式.这次虽说是.net core实践,但是核心点使用jquery居多. 建议看这篇文章的朋友可 ...

  9. .net core 使用阿里云短信发送SMS

    阿里云官方的skd(aliyun-net-sdk-core,aliyun-net-sdk-dysmsapi)在dnc中发送短信会出错,nuget上的包貌似也一样不管用.直接改下sdk当然也可以,但就发 ...

随机推荐

  1. 13、多进程multiprocessing、进程池

    内容相关: multiprocessing: 进程的创建与运行 进程常用相关函数 进程池: 为什么要有进程池 进程池的创建与运行:串行.并行 回调函数 多进程multiprocessing: pyth ...

  2. 我的博客:C# PHP J2ee Java Android js WP Asp.net mvc Python

    <p><A target="_blank" href="http://blog.163.com/hr_company_product/" &g ...

  3. 慕学在线网0.5_xadmin的全局配置

    全局配置包括了以下修改: 开启主题功能: 修改左上角的"django Xadmin"和主界面的"我的公司": App菜单收叠: App名字修改(汉化). 1.把 ...

  4. C#异常--System.IO.FileLoadException:“混合模式程序集是针对“v2.0.50727”版的运行时生成的错误

    异常信息: System.IO.FileLoadException:“混合模式程序集是针对“v2.0.50727”版的运行时生成的,在没有配置其他信息的情况下,无法在 4.0 运行时中加载该程序集.” ...

  5. Java同步、异步区别

    一.概念: 1.同步:所有的操作都做完,才返回给用户.这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的感觉).这种情况下,用户不 ...

  6. 【PAT】B1003 我要通过!

    我觉得这是PAT中最坑的一道题,表述令人很不适应 分析过程: 条件1.只有P,A,T三种字符 条件2.xPATx正确,x可以是空串,或者由A组成的字符串 条件3.如果aPbTc是正确的,aPbATca ...

  7. January 24th, 2018 Week 04th Wednesday

    Each day has enough trouble of its own. 一天的难处一天当. Looking into the sunset I can't help but notice th ...

  8. call()和apply()

    call()和apply()方法类似,区别是,call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组. 当一个函数在其主体中使用 this 关键字时,可以通过使用 ...

  9. cf C. Finite or not? 数论

    You are given several queries. Each query consists of three integers pp, qq and bb. You need to answ ...

  10. 微软公布针对最新IE漏洞的安全通报2963983

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/MSSecurity/article/details/24637607  微软于昨天公布了一篇最新 ...