
从事Asp.Net开发13年,先后曾任职于5173、盛大文学、IPS、百强旅游公司担任技术开发工作,目前就职于墨西哥最大的软件开发商Softtek,担任架构师,为康宝莱中国提供技术支持与重构。开源项目“微信快速开发框架”负责人,目前正在研究.Net Core与微服务架构。
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
引言 What is the Health Check Health Check(健康状态检查)不仅是对自己应用程序内部检测各个项目之间的健康状态(各项目的运行情况、项目之间的连接情况等),还包括了应用程序对外部或者第三方依赖库的状态检测。 Why use Health Check 现在我们的项目越来越多的从单体多层架构转换成多项目多层架构即现在流行的微服务架构。 原来我们的App把各个模块分层分项目处理,比如Users项目仅仅处理User的一些业务需求,但在整个项目使用的时候,我们仅仅需要引用其类库即可,不用担心项目与类库之间的不兼容问题,如果不兼容在编译期已经会有提示。但如今,业务规模越来越庞大的时候,我们单独把Users作为一个service来做,所有一切都在其内部处理,对于外部来说仅仅公开几个api即可,但与项目之间的连接就从单纯的物理引用关系转换成了网络调用关系。 当我们架构从单体架构到微服务架构的时候,我们会发现越来越多的引用从物理转向了网络,在原来我们不需要考虑之间是否调用成功,但现在我们必须考虑进去,网络因素、服务器因素、其他因素等都会影响各服务之间的调用,因此Health Check孕育而生,它在微服务架构中是举足轻重的。 Health Check’s Feathure Health Check的功能有哪些?在微服务架构中很简单,就是检查各services的运行状态是否正常。在微服务的架构中,所有的一切都是service,db is service,rabbitmq is service,auth is service, shoppingcart is server……我们的架构能够根据业务需求,横向的扩容,多个db,多个rabbitmq,多个auth,多个shoppingcart。我们总结下,微服务架构下的Health Check是通过网络检查各services是否正常运行,它的功能是: 1、提供外部调用Health Check接口,反馈自身状态 2、检测相关service状态是否正常(比如db server,能否连接到db,能否打开数据库等) 3、UnHealthly时处理机制 Health Check in eShop Why in eShop? 之前我们一直都在介绍eShop是微软基于微服务架构的.Net Core Demo,为了保障各个services之间的调用正常,所以Health Check是必不可少的。 Where is it? 在Demo中,我们可以在各个services中都能看到HealthCheck,可以说是无处不在,在系列【二】和【三】中我们都有见过。在eShop项目中,我们可以看到有个HealthChecks目录,其中包含了与HealthChecks相关的几个项目: Microsoft.Extensions.HealthChecks ------------ Health Check的核心代码 Microsoft.AspNetCore.HealthChecks ------------ Asp.Net Core注册扩展类库 Microsoft.Extensions.HealthChecks.AzureStorage ----- 扩展对Azure Blob Storage的支持 Microsoft.Extensions.HealthChecks.SqlServer ------ 扩展对MsSql Server的支持 通过代码了解,在eShop中实现了对各Api的通讯检测和SqlServer、AzureBlobStorage的检测,但其中并没有看到对重试机制和UnHealthy时的处理,相信以后会加入这些,目前微软已经单独为HealthChecks开了一个Repository,这样你就可以单独引用到自己的项目中,非常棒的东西。 在项目中,我们一般只会在Program.cs和Startup.cs看到跟HealthChecks相关的代码。目前仅在客户端(其他service或者我们的app)请求我们的HealthChecks的时候,我们会进行相关service的检测,然后再返回自身的一个状态码。 How use the Healthchecks? 接下来我们看下在eShop中代码是如何使用的,我们以Identity.Api为例,在之前的文章中我们提到过,在Program.cs中,有一段UseHealthChecks("/hc"),我们跟踪下代码,你会看到它会先判断path是否负责规则,如果符合的话就会通过IWebHostBuilder注册一个HealthCheckStartupFilter,Filter则会把相应的HealthCheckMiddleware注册到管道中,我们看下主要源码: public async Task Invoke(HttpContext context) { if (IsHealthCheckRequest(context)) { var timeoutTokenSource = new CancellationTokenSource(_timeout); var result = await _service.CheckHealthAsync(timeoutTokenSource.Token); var status = result.CheckStatus; if (status != CheckStatus.Healthy) context.Response.StatusCode = 503; context.Response.Headers.Add("content-type", "application/json"); await context.Response.WriteAsync(JsonConvert.SerializeObject(new { status = status.ToString() })); return; } else { await _next.Invoke(context); } } private bool IsHealthCheckRequest(HttpContext context) { if (_port.HasValue) { var connInfo = context.Features.Get<IHttpConnectionFeature>(); if (connInfo.LocalPort == _port) return true; } if (context.Request.Path == _path) { return true; } return false; } 它会先检测这个请求是不是HealthCheck请求,如果不是则走下面的步骤,如果是,则会进一步进行对相关service的HealthChecks。对相关service的Config实在Startup.cs中进行的: services.AddHealthChecks(checks => { var minutes = 1; if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) { minutes = minutesParsed; } checks.AddSqlCheck("Identity_Db", Configuration.GetConnectionString("DefaultConnection"), TimeSpan.FromMinutes(minutes)); }); 这里可以看到,在Identity.Api中,仅仅配置了对数据库的检测。 ok,我们非常简单的在项目中引用了HealthCheck,当我们的api运行后,我们只需要通过 http://xxx/hc 就能对这个api进行Health Check了。 写在最后 今天我们了解了Health Check,并简单看了它在eShop中的使用。目前看来还不是很完善,只在其他service或者app调用其Health Check接口的时候才能进行检测,当然我们可以改造下,使其在程序运行的时候先检测一次。在eShop中我们并没有看到在UnHealth的时候的处理,这个扩展起来很简单,你可以通过自身需求,进行日志,email,短信都可以,后面可以找机会实现下。 乘着不忙的时候赶紧学习,如果大家有兴趣学习.Net Core的话,可以加QQ群:376248054(通关密码:cnblogs)。另外喜欢微服务的朋友可以看下园中大神Savorboard的微服务系列
在目前的主流架构中,我们越来越多的看到web Api的存在,小巧,灵活,基于Http协议,使它在越来越多的微服务项目或者移动项目充当很好的service endpoint。 问题 以Asp.Net Web Api 为例,随着业务的扩展,产品的迭代,我们的web api也在随之变化,很多时候会出现多个版本共存的现象,这个时候我们就需要设计一个支持版本号的web api link,比如:原先:http://www.test.com/api/{controller}/{id} 如今:http://www.test.com/api/{version}/{controller}/{id} 在我们刚设计的时候,有可能没有考虑版本的问题,我看到很多的项目都会在link后加入一个“?version=”的方式,这种方式确实能够解决问题,但对Asp.Net Web Api来说,进入的还是同一个Controller,我们需要在同一个Action中进行判断版本号,例如: http://www.test.com/api/bolgs?version=v2[HttpGet] public class BlogsController : ApiController { // GET api/<controller> public IEnumerable<string> Get([FromUri]string version = "") { if (!String.IsNullOrEmpty(version)) { return new string[] { $"{version} blog1", $"{version} blog2" }; } return new string[] { "blog1", "blog2" }; } } 我们看到我们通过判断url中的version参数进行对应的返回,为了确保原先接口的可用,我们需要对参数赋上默认值,虽然能够解决我们的版本迭代问题,但随着版本的不断更新,你会发现这个Controller会越来越臃肿,维护越来越困难,因为这种修改已经严重违反了OCP(Open-Closed Principle),最好的方式是不修改原先的Controller,而是新建新的Controller,放在对应的目录中(或者项目中),比如: 为了不影响原先的项目,我们尽量不要改动原Controller的Namespace,除非你有十足的把握没有影响,不然请尽量只是移动到目录。 ok,为了保持原接口的映射,我们需要在WebApiConfig.Register中注册支持版本号的Route映射: config.Routes.MapHttpRoute( name: "DefaultVersionApi", routeTemplate: "api/{version}/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); 打开浏览器或者postman,输入原先的api url,你会发现这样的错误: 那是因为web api 查找Controller的时候,只会根据ClassName进行查找的,当出现相同ClassName的时候,就会报这个错误,这时候我们就需要打造自己的Controller Selector,好在微软留了一个接口给到我们:IHttpControllerSelector。不过为了兼容原先的api(有些不在我们权限范围内的api,不加版本号的那种),我们还是直接集成DefaultHttpControllerSelector比较好,我们给定一个规则,不负责我们版本迭代的api,就让它走原先的映射。 思路 1、项目启动的时候,先把符合条件的Controller加入到一个字典中 2、判断request,符合规则的,我们返回我们制定的controller。 打造属于自己的Selector 思路有了,那改造起来也非常简单,今天我们先做一个简单的,等有时间改成可配置的。 第一步,我们先创建一个Selector类,继承自DefaultHttpControllerSelector,然后初始化的时候创建一个属于我们自己的字典: public class VersionHttpControllerSelector : DefaultHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _lazyMappingDictionary; private const string DefaultVersion = "v1"; //默认版本号,因为之前的api我们没有版本号的概念 private const string DefaultNamespaces = "WebApiVersions.Controllers"; //为了演示方便,这里就用到一个命名空间 private const string RouteVersionKey = "version"; //路由规则中Version的字符串 private const string DictKeyFormat = "{0}.{1}"; public VersionHttpControllerSelector(HttpConfiguration configuration):base(configuration) { _configuration = configuration; _lazyMappingDictionary = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDict); } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDict() { var result = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); var assemblies = _configuration.Services.GetAssembliesResolver(); var controllerResolver = _configuration.Services.GetHttpControllerTypeResolver(); var controllerTypes = controllerResolver.GetControllerTypes(assemblies); foreach(var t in controllerTypes) { if (t.Namespace.Contains(DefaultNamespaces)) //符合NameSpace规则 { var segments = t.Namespace.Split(Type.Delimiter); var version = t.Namespace.Equals(DefaultNamespaces, StringComparison.OrdinalIgnoreCase) ? DefaultVersion : segments[segments.Length - 1]; var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); var key = string.Format(DictKeyFormat, version, controllerName); if (!result.ContainsKey(key)) { result.Add(key, new HttpControllerDescriptor(_configuration, t.Name, t)); } } } return result; } } 有了字典接下来就好办了,只需要分析request就好了,符合我们版本要求的,就从我们的字典中查找对应的Descriptor,如果找不到,就走默认的,这里我们需要重写SelectController方法: public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) throw new HttpResponseException(HttpStatusCode.NotFound); var controllerName = GetControllerName(request); if (String.IsNullOrEmpty(controllerName)) throw new HttpResponseException(HttpStatusCode.NotFound); var version = DefaultVersion; if (IsVersionRoute(routeData, out version)) { var key = String.Format(DictKeyFormat, version, controllerName); if (_lazyMappingDictionary.Value.ContainsKey(key)) { return _lazyMappingDictionary.Value[key]; } throw new HttpResponseException(HttpStatusCode.NotFound); } return base.SelectController(request); } private bool IsVersionRoute(IHttpRouteData routeData, out string version) { version = String.Empty; var prevRouteTemplate = "api/{controller}/{id}"; object outVersion; if(routeData.Values.TryGetValue(RouteVersionKey, out outVersion)) //先找符合新规则的路由版本 { version = outVersion.ToString(); return true; } if (routeData.Route.RouteTemplate.Contains(prevRouteTemplate)) //不符合再比对是否符合原先的api路由 { version = DefaultVersion; return true; } return false; } 完成这个类后,我们去WebApiConfig.Register中进行替换操作: config.Services.Replace(typeof(IHttpControllerSelector), new VersionHttpControllerSelector(config)); ok,再次打开浏览器,输入http://www.xxx.com/api/blogs 和 http://www.xxx.com/api/v2/blogs ,这时应该能看到正确的执行: 写在最后 今天我们打造了一个简单符合webapi版本号更新迭代的ControllerSelector,不过还不是很完善,因为很多都是hard code,后面我会做一个支持配置的ControllerSelector放到github上。 之前一直在研究eShopOnContrainers,最近也在研究,不过工作确实有点忙,见谅见谅,如果大家.Net有什么问题或者喜欢技术交友的,都可以加QQ群:376248054
引言 大家好像对分析源码厌倦了,说实在我也会厌倦,不过不看是无法分析其后面的东西,从易到难是一个必要的过程。 今天说下EventBus,前几天园里的大神已经把其解刨,我今天就借着大神的肩膀,分析下在eShop项目中EventBus的实现。 最近发觉转发文章不写出处的,特此加上链接:http://inday.cnblogs.com 解析源码 我们知道使用EventBus是为了解除Publisher和Subscriber之间的依赖性,这样我们的Publisher就不需要知道有多少Subscribers,只需要通过EventBus进行注册管理就好了,在eShop项目中,有一个这样的接口IEventBus(eShopOnContainers\src\BuildingBlocks\EventBus\EventBus\Abstractions) public interface IEventBus { void Subscribe<T, TH>(Func<TH> handler) where T : IntegrationEvent where TH : IIntegrationEventHandler<T>; void Unsubscribe<T, TH>() where TH : IIntegrationEventHandler<T> where T : IntegrationEvent; void Publish(IntegrationEvent @event); } 我们可以看到这个接口定义了EventBus所需的一些操作, 对比大神的EventBus,相关功能都是一致的,我们看下它的实现类:EventBusRabbitMQ,从名字上可以看出,这是一个通过RabbitMQ来进行管理的EventBus,我们可以看到它使用了IEventBusSubscriptionsManager进行订阅存储,也就是大神文中的: private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; 微软在Demo中把其提取出了接口,把一些常用方法给提炼了出来,但是核心还是Dictionary<string, List<Delegate>>, 使用Dictionary进行Map映射。通过Subscribe和UnSubscribe进行订阅和取消,使用Publish方法进行发布操作。 public void Subscribe<T, TH>(Func<TH> handler) where T : IntegrationEvent where TH : IIntegrationEventHandler<T> { var eventName = typeof(T).Name; var containsKey = _subsManager.HasSubscriptionsForEvent<T>(); if (!containsKey) { if (!_persistentConnection.IsConnected) { _persistentConnection.TryConnect(); } using (var channel = _persistentConnection.CreateModel()) { channel.QueueBind(queue: _queueName, exchange: BROKER_NAME, routingKey: eventName); } } _subsManager.AddSubscription<T, TH>(handler); } 我们看到在订阅的时候,EventBus会检查下在Map中是否有相应的注册,如果没有的话首先回去RabbitMQ中创建一个新的channel进行绑定,随后在Map中进行注册映射。 UnSubscribe则直接从Map中取消映射,通过OnEventRemoved事件判断Map下此映射的subscriber是否为空,为空则从RabbitMQ中关闭channel。 在RabbitMQ的构造方法中,我们看到这样一个创建:CreateConsumerChannel(),这里创建了一个EventingBasicConsumer,当Queue中有新的消息时会通过ProcessEvent执行Map中注册的handler(subscribers),看图可能更清晰些: 在ProcessEvent方法中,回去Map中找寻subscribers,然后通过动态反射进行执行: private async Task ProcessEvent(string eventName, string message) { if (_subsManager.HasSubscriptionsForEvent(eventName)) { var eventType = _subsManager.GetEventTypeByName(eventName); var integrationEvent = JsonConvert.DeserializeObject(message, eventType); var handlers = _subsManager.GetHandlersForEvent(eventName); foreach (var handlerfactory in handlers) { var handler = handlerfactory.DynamicInvoke(); var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType); await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent }); } } } 微软通过简单的代码解耦了Publisher和Subscribers之间的依赖关系,我们引用大神的总结: 应用 在catalog.api中,微软出现了EventBus,我在上一篇中也提到了,这是我的一个疑惑,因为在catalog中并没有订阅操作,直接执行了Publish操作,原先以为是一个空操作,后来看了Basket.Api我才知道为何微软要用RabbitMQ。 使用RabbitMQ,我们不仅是从类之间的解耦,更可以跨项目,跨语言,跨平台的解耦,publisher仅仅需要把消息体(IntegrationEvent)传送到RabbitMQ,Consumer从Queue中获取消息体,然后推送到Subscribers执行相应的操作。我们看下Basket.Api.Startup.cs: protected virtual void ConfigureEventBus(IApplicationBuilder app) { var catalogPriceHandler = app.ApplicationServices .GetService<IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>>(); var orderStartedHandler = app.ApplicationServices .GetService<IIntegrationEventHandler<OrderStartedIntegrationEvent>>(); var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler> (() => app.ApplicationServices.GetRequiredService<ProductPriceChangedIntegrationEventHandler>()); eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler> (() => app.ApplicationServices.GetRequiredService<OrderStartedIntegrationEventHandler>()); } 在这个方法里,我们看到了Subscribe操作,想想之前的提问有点搞笑,不过研究明白了也不错,对吧! 总结 今天我们看了EventBus在Demo中的应用,总结一下。 1、EventBus可以很好的解耦订阅者和发布者之间的依赖 2、使用RabbitMQ能够跨项目、跨平台、跨语言的解耦订阅者和发布者 虽然在Demo中我们看到对订阅者的管理是通过Dictionary内存的方式,所以我们的Subscribe仅仅只在Basket.Api中看到,但微软是通过IEventBusSubscriptionsManager接口定义的,我们可以通过自己的需求来进行定制,可以做成分布式的,比如使用memcached。 写在最后 每个月到下旬就会比较忙,所以文章发布会比较慢,但我也会坚持学习完eShop的,为了学习,我建了个群,大家可以进来一起学习,有什么建议和问题都可以进来哦。 eShop虽好,但不建议大家放到生产环境,毕竟是一个Demo,而且目前还是ALPHA版本,用来学习是一个很好的教材,这就是一个大杂烩,学习中你会学到很多新的东西,大家如果看好core的发展,可以一起研究下。 QQ群:376248054
上一篇我们说了Identity Service,因为其基于IdentityServer4开发的,所以知识点不是很多,今天我们来看下Catalog Service,今后的讲解都会把不同的、重点的拿出来讲,希望大家明白。 源码分析 我们先看下它的目录结构,很标准的webapi目录: 首先看下Program,跟IdentityService类似,多了一个UseWebRoot(“Pics”),把pics这个目录设置成了webroot,其他都一样。 在Startup的构造方法中,我们也看到了使用了secret manager tool,但是多了一个参数,在这里我们看到的是Assembly类型,其实secret只需要其中的userSecretsId而已。 在ConfigureServices中,我们看到如下代码: services.AddMvc(options => { options.Filters.Add(typeof(HttpGlobalExceptionFilter)); }).AddControllersAsServices(); 添加了一个filter,这个HTtpGlobalExceptionFilter可以在项目中找到,大概的意思就是遇到抛出CatalogDomainException类型的错误时,返回特定的错误码。 AddControllersAsServices这个扩展方法是把项目中的Controller都注册到Services中,我们看下源码: public static IMvcCoreBuilder AddControllersAsServices(this IMvcCoreBuilder builder) { var feature = new ControllerFeature(); builder.PartManager.PopulateFeature(feature); foreach (var controller in feature.Controllers.Select(c => c.AsType())) { builder.Services.TryAddTransient(controller, controller); } builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>()); return builder; } 中间那段foreach就是,这样我们在项目中通过依赖注入方式都能方便的访问到各个controller了。 Going down: services.AddDbContext<CatalogContext>(options => { options.UseSqlServer(Configuration["ConnectionString"], sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency sqlOptions.EnableRetryOnFailure(maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }); // Changing default behavior when client evaluation occurs to throw. // Default in EF Core would be to log a warning when client evaluation is performed. options.ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning)); //Check Client vs. Server evaluation: https://docs.microsoft.com/en-us/ef/core/querying/client-eval }); 对DBContext的配置的时候,这里使用了Connection Resiliency(弹回连接)的方式,其中可以看到使用migration的时候,它使用了MigrationsAssembly(AssemblyName),这种方式跟我之前讲的FluentNhibernate有点类似,EnableRetryOnFailure设置了这个Action的失败尝试机制,如果Migration的时候遇到Failure,就会自动重试,这种方式避免了app与database分离造成的连接偶尔失败造成的影响。为什么会有这个机制呢?因为当我们的database在云端的时候,比如Azure SQL,不可避免的会出现网络连接问题,即使我们把app和database放在一个数据中心中,我相信偶尔也会有这个问题,我们现在可以通过配置,使其如果遇到失败就会重新操作,一定程度避免了网络偶尔造成的问题。你也可以设置一些策略,使其能够在运行命令的时候能够进行重试EF默认情况下只是记录client evaluation中的warns,我们可以通过ConfigureWarnings使其抛出这个警告,你也可以配置成忽略。 接下来我们看到如下代码: services.Configure<CatalogSettings>(Configuration); 我们可以在eShop的各个项目中都能找到类似的语句,它会把一些项目相关的Settings注册到services中,使其成为环境变量,我们可通过setting.json进行配置。除了通过setting.json进行配置,我们还能通过Docker run –e 进行灵活化配置。 在这里我们的CatalogSetting含有一个ExternalCatalogBaseUrl属性,我们在docker run的时候可以输入如下命令: docke run -e "ExternalCatalogBaseUrl=http://localhost:5011/" .... 这样就能灵活的通过docker命令进行配置了,非常方便,我们也可以通过-e对我们setting.json中的变量进行赋值,比如ConnectionString,你可以通过点击了解更多相关内容。 // Add framework services. services.AddSwaggerGen(); services.ConfigureSwaggerGen(options => { options.DescribeAllEnumsAsStrings(); options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info() { Title = "eShopOnContainers - Catalog HTTP API", Version = "v1", Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", TermsOfService = "Terms Of Service" }); }); services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); 上面两段代码,分别配置了SwaggerGen和Cors(跨域)策略,SwaggenGen是一个非常实用的框架,它能自动把我们的api转为web方式呈现在我们眼前,还能进行调试,非常好用。Cors的配置这里用的不好,它允许了所有请求,建议还是按照实际需求来吧,否则没有跨域设置的意义了。 接下来我们看到了一系列的add service的操作,都是关于EventBus的,稍微看了下,发现目前只做了log的动作,我们看下代码: if (raiseProductPriceChangedEvent) // Save and publish integration event if price has changed { //Create Integration Event to be published through the Event Bus var priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id, productToUpdate.Price, oldPrice); // Achieving atomicity between original Catalog database operation and the IntegrationEventLog thanks to a local transaction await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(priceChangedEvent); // Publish through the Event Bus and mark the saved event as published await _catalogIntegrationEventService.PublishThroughEventBusAsync(priceChangedEvent); } 上面的代码意思是在价格有变动的时候,我们就调用EventService进行保存,同时对操作进行了记录。PublishThroughEventBusAsync方法则对这条记录的State更改为published。目前来说我不太清楚为何要用这种方式,也不知道为何取名为EventBus,不过我在项目的issue中已经提出了这个问题,希望项目的开发者们能给我一个答案。我有查看了Basket.Api,在这个项目中会有订阅行为,具体的等到下一章我们再仔细看看。 ok,我们再看下Configure方法,下面一段代码我们可以学习下: var context = (CatalogContext)app .ApplicationServices.GetService(typeof(CatalogContext)); WaitForSqlAvailability(context, loggerFactory); 我们看到在这里它调用了之前注册的CatalogContext,它并没有通过new进行实例化,而是通过GetService的方式获取之前的注册,这样context所依赖的其他实例也一并带进来了,非常方便好用。 WaitForSqlAvailability方法是对数据库可用进行尝试,因为后面它需要进行数据迁移。 CatalogService包含了2个Controller,一个是PicController,一个是CatalogController,PicController仅仅是根据ID获取了图片,CatalogController展示了用webapi如何做CURD。 运行部署 如果你要运行Catalog.Api,你必须安装MSSQL和RabbitMQ,这次我把我的系统换成了Win10 Pro,并在电脑上使用Docker安装了MSSQL-Server-Linux和RabbitMQ。安装这2个非常简单,仅仅需要输入几条命令即可: docker run --name mssql -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Pass@word' -p 5433:1433 -d microsoft/mssql-server-linux docker run -d --hostname my-rabbit --name rabbitmq -p 8080:15672 -p 5672:5672 rabbitmq:3-management ok,我们使用docker创建了mssql和rabbitmq,这里注意一下,我把mssql的端口映射到了本机的5433上,还有rabbitmq的管理页面,我映射到了本机的8080端口,你可以通过http://localhost:8080 进行访问。 上一篇我们说过我们可以通过iisexpress/Kestrel或者docker的形式运行因为牵涉到配置,所以这两种方式的运行有些不同。 一、iisExpress或Kestrel方式下,因为刚刚我们把mssql和rabbitmq的端口都映射到了本机,所以我们只需要在setting.json中把数据库连接和rabbitmq的地址指向本机即可,如下: { "ConnectionString": "Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word", "ExternalCatalogBaseUrl": "http://localhost:5101", "EventBusConnection": "localhost", "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } } ok,Ctrl+F5,运行一下看看: 当看到上面这个页面,说明你的运行正常了,你还得测试下api是否运行正常,比如Pic,比如Items。 二、docker中运行,参照上一篇的方式,先publish再build image, 不过这里要注意一点,因为你之前的ConnectionString和EventBusConnection都是指向本机(127.0.0.1)的,所以这里必须改一下,改成主机的ip地址或者是对应容器的ip也可以,如果您不想更改的话,也可以通过docker -e进行设置,比如: docker run -p 8899:80 --name catalog -e "EventBusConnection=172.17.0.2" -d catalog:01 我这里的172.17.0.2是我rabbitmq容器的ip地址,你可以通过docker inspect containerId 进行查看容器的ip。 如果一切配置都正确的话,你就可以通过浏览器http://localhost:8899 进行浏览了。 当然,除了正常浏览外,你还需测试下api是否正常。 困惑 在这个项目中有一些疑惑,希望大家能够给我答案。 Connection Resiliency,我看了很久,字面意思是弹性连接,但我觉得用弹性好像不太适合,一般来讲我们说的弹性都是指架构或者系统的伸缩性,我一开始也是从这个角度去了解,但看了很多文章,觉得它只是让我们在启动的时候,设置一些重试策略,在后面调用中可使用此策略,策略会根据你设置的重试次数、延迟时间等去自动重试,避免因为偶尔的错误造成的影响,所以觉得用弹回比较恰当。 EventBus,我感觉很奇怪,为什么一定要取这个名字呢?在Android中,很明确的,它是进行订阅发布,消息传递,可以解耦发布者和订阅者,但在Catalog.Api里,变成了记录操作,没有看到解耦,也没有看到订阅。在我的理解中,应该在Startup进行订阅操作,发布者CatalogController在进行update操作的时候,订阅者进行add log动作,但在这个实例中,我看到的是同步进行了这些操作,所以很不解。 Mssql-server-linux,当你用Docker安装了以后,你却不能使用visual studio 2017的sql server data tools进行查询(只能进行连接),为了查看效果,还需要安装Microsoft Sql Server Management Studio(必须17版本以后)进行查看数据。 写在最后 这次的文章来的比较晚,一方面有点忙,另一方面就是上面提到的困惑,面对困惑我试着去解答,但有时候真的无法解答,所以提出来集思广益。 后面可能会比较慢,需要学习的东西真多,一边写一边学习成为这次系列的乐趣,现在每天坚持6公里快走,夜走能够是我保持头脑清晰,思考项目中的疑问,现在发觉生活越发有趣。 或许有很多人觉得只看了Startup就够了吗?其实真不够,我目前先把框架的源码过一遍,后面会分篇讲述,比如Connection Resiliency。 最后应大家要求,我建了一个QQ群:376248054,大家可以进来一起探讨,一起学习!