ASP.NET Core 运行原理解剖[1]:Hosting

SP.NET Core 是新一代的 ASP.NET,第一次出现时代号为 ASP.NET vNext,后来命名为ASP.NET 5,随着它的完善与成熟,最终命名为 ASP.NET Core,表明它不是 ASP.NET 的升级,而是一个重新设计的Web开发框架。而它一个非常重要的变化就是它不再依赖于IIS,而是一个独立的自寄宿的控制台应用程序,这也是它可以跨平台的基石,而本文就来详细探讨一下 ASP.NET Core 的启动过程。

目录

本系列文章将会从源码分析来讲解 ASP.NET Core 的运行原理,分为以下几个章节:

ASP.NET Core 运行原理解剖[1]:Hosting(Current)

ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍

ASP.NET Core 运行原理解剖[3]:Middleware-请求管道的构成

ASP.NET Core 运行原理解剖[4]:进入HttpContext的世界(待续)

ASP.NET Core 运行原理解剖[5]:Authentication(待续)

前言

我们先回顾一下以前的 ASP.NET 是怎么来运行的:

ASP.NET 是严重依赖于IIS的,System.Web 中有很多方法都是直接调用的 IIS API,并且它还是驻留在IIS进程中的。而 ASP.NET Core 的运行则是一个完全独立的控制台程序,它有自己的 Kestrel Server,可以直接对外部提供服务。

不过 Kestrel 的功能相对较于简单,所以我们还是需要一个反向代理服务器将 Kestrel 服务器保护起来。而微软也为我们提供了 UseIISIntegration 方法,方便与IIS进行集成。因此,在 Windows 下,通常还是使用IIS来部署,那么,此时与 ASP.NET 的运行方式又有什么区别呢?

通过上图,可以很清楚的明白它们的区别。在 ASP.NET Core 中,IIS 是通过 HTTP 的方式来调用我们的 ASP.NET Core 程序。而部署在IIS中时,并不需要我们手动来启动 ASP.NET Core 的控制台程序,这是因为IIS新增了一个 AspNetCoreModule 模块,它负责 ASP.NET Core 程序的启动与停止,并能监听 ASP.NET Core 程序的状态,在我们的应用程序意外崩溃时重新启动。

下面开始进入正题,进入到 ASP.NET Core 的代码中去。

WebHost的创建

对于一个程序控制台程序来说,它的入口点便是 Program 中的 Main 方法,ASP.NET Core 程序自然也不例外:

public class Program{   
 public static void Main(string[] args)    {BuildWebHost(args).Run();}    
 
 public static IWebHost BuildWebHost(string[] args) =>WebHost.CreateDefaultBuilder(args).UseStartup<Startup>().Build(); }

WebHost.CreateDefaultBuilder 是在 2.0 中新增的,在 MetaPackages 程序集中,代码如下:

public static IWebHostBuilder CreateDefaultBuilder(string[] args){   
 var builder = new WebHostBuilder().UseKestrel().UseContentRoot(Directory.GetCurrentDirectory()).ConfigureAppConfiguration((hostingContext, config) =>{        
    var env = hostingContext.HostingEnvironment;        
       config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
     .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);        
         if (env.IsDevelopment()){            
            var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));        
                if (appAssembly != null){config.AddUserSecrets(appAssembly, optional: true);}}config.AddEnvironmentVariables();      
            if (args != null){config.AddCommandLine(args);}}).ConfigureLogging((hostingContext, logging) =>{logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));logging.AddConsole();logging.AddDebug();}).UseIISIntegration().UseDefaultServiceProvider((context, options) =>{options.ValidateScopes = context.HostingEnvironment.IsDevelopment();});    return builder; }

它只是用来简化我们的代码,做了一些基本的配置:

  1. 注册 Kestrel 中间件,指定 WebHost 要使用的 Server(HTTP服务器)。

  2. 设置 Content 根目录,将当前项目的根目录作为 ContentRoot 的目录。

  3. 读取 appsettinggs.json 配置文件,开发环境下的 UserSecrets 以及环境变量和命令行参数。

  4. 读取配置文件中的 Logging 节点,对日志系统进行配置。

  5. 添加 IISIntegration 中间件。

  6. 设置开发环境下, ServiceProvider 的 ValidateScopes 为 true,避免直接在 Configure 方法中获取 Scope 实例。

然后指定 Startup 类,最后通过 Build 方法创建 WebHost 对象,而WebHostBuilder 的代码较多,感兴趣的可以去看完整代码: WebHostBuilder,而在这里我只展示部分代码片段来帮助理解:

public IWebHost Build(){    
       var hostingServices = BuildCommonServices(out var hostingStartupErrors);  
   var applicationServices = hostingServices.Clone();  
   var hostingServiceProvider = hostingServices.BuildServiceProvider();AddApplicationServices(applicationServices, hostingServiceProvider); }

Build 中的 BuildCommonServices 方法主要有两个功能:

首先在程序集中查找 HostingStartupAttribute:

if (!_options.PreventHostingStartup)
{    var exceptions = new List<Exception>();    // Execute the hosting startup assembliesforeach (var assemblyName in _options.HostingStartupAssemblies){       
       var assembly = Assembly.Load(new AssemblyName(assemblyName));               foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>()){          
           var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);hostingStartup.Configure(this);}} }

HostingStartupAttribute 给我们一个在其它程序集中做一些启动配置的机会,在我们进行多层开发及模块化的时候非常有用,下一站会详细解释。

然后便是查找我们的 Startup 类:

if (!string.IsNullOrEmpty(_options.StartupAssembly))
{   
    var startupType = StartupLoader.FindStartupType(_options.StartupAssembly, _hostingEnvironment.EnvironmentName);
    if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())){services.AddSingleton(typeof(IStartup), startupType);}    
   else{services.AddSingleton(typeof(IStartup), sp =>{      
           var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();    
          var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);    
          return new ConventionBasedStartup(methods);});} }

首先是判断是否有 _options.StartupAssembly,对应配置文件中的 "startupAssembly" ,如果我们没有设置,那便是空的,并不会执行上面代码。通常我们会使用 UseStartup<Startup> 的方法来注册 Startup 类,而他们的作用是一样的,都是将我们的 Startup 类做为一个单例注册到了 DI 系统。

而最终 BuildCommonServices 返回一个 IServiceCollection,用于构建 hostingServiceProvider:

var hostingServices = BuildCommonServices(out var hostingStartupErrors);
var applicationServices = hostingServices.Clone();
var hostingServiceProvider = hostingServices.BuildServiceProvider();

接下来创建 WebHost :

public IWebHost Build(){    
     var host = new WebHost(applicationServices,hostingServiceProvider,_options,_config,hostingStartupErrors);}host.Initialize();  
   return host; }

这里需要说明的,hostingServiceProvider 是 ASP.NET Core 中的第一个 ServiceProvider,也是根 ServiceProvider,但它是在我们的 Starpup 类执行之前创建的,也就是说并不会包含我们在 ConfigureServices 中注册的服务(但包含使用 HostingStartupAttribute 注册的服务)。

WebHost启动流程

在上一步,创建完 WebHost 之后,便调用它的 Run 方法,而 Run 方法再去调用 WebHost 的 StartAsync 方法,开始 ASP.NET Core 的启动工作,主要包含以下几个步骤:

1. 初始化,构建 RequestDelegate

RequestDelegate 是我们的应用程序处理请求,输出响应的整个过程,也就是我们的 ASP.NET Core 请求管道。

而它有如下定义:

public delegate Task RequestDelegate(HttpContext context);

这里不再对 RequestDelegate 进行过多的介绍,以后会详细解释。

1.1. 调用 Startup 中的 ConfigureServices 方法

在前面介绍过,我们的 Startup 类已经注册到了 ASP.NET Coer 的 DI 系统中,因此可以直接从 DI 中获取:

private IStartup _startup;
private IServiceProvider _applicationServices;_startup = _hostingServiceProvider.GetRequiredService<IStartup>();_applicationServices = _startup.ConfigureServices(_applicationServiceCollection);

这里使用的 _hostingServiceProvider 是我们在 WebHost 中创建的根 ServieProvider。

1.2. 初始化 Http Server

Server 是一个HTTP服务器,负责HTTP的监听,接收一组 FeatureCollection 类型的原始请求,并将其包装成 HttpContext 以供我们的应用程序完成响应的处理。

public interface IServer : IDisposable{IFeatureCollection Features { get; }Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken);  
   Task StopAsync(CancellationToken cancellationToken); }

而上面注册的 Kestrel 便是默认的 Server :

public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder){hostBuilder.UseLibuv();   
   return hostBuilder.ConfigureServices(services =>{services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>();services.AddSingleton<IServer, KestrelServer>();}); }

Server的初始化主要是配置要监听的地址:

private void EnsureServer(){    
   if (Server == null){Server = _applicationServices.GetRequiredService<IServer>();        var serverAddressesFeature = Server.Features?.Get<IServerAddressesFeature>();      
       var addresses = serverAddressesFeature?.Addresses;    
      if (addresses != null && !addresses.IsReadOnly && addresses.Count == 0){        
            var urls = _config[WebHostDefaults.ServerUrlsKey] ?? _config[DeprecatedServerUrlsKey];        
           if (!string.IsNullOrEmpty(urls)){serverAddressesFeature.PreferHostingUrls = WebHostUtilities.ParseBool(_config, WebHostDefaults.PreferHostingUrlsKey);                foreach (var value in urls.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)){addresses.Add(value);}}}} }

Addresses 默认是通过在 launchSettings.json 中来查找的。

1.3. 创建 IApplicationBuilder

IApplicationBuilder 用于构建应用程序的请求管道,也就是生成 RequestDelegate,有如下定义:

public interface IApplicationBuilder{IServiceProvider ApplicationServices { get; set; }IFeatureCollection ServerFeatures { get; }IDictionary<string, object> Properties { get; }   
    RequestDelegate Build();    IApplicationBuilder New();    IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); }

而它的创建过程是通过 ApplicationBuilderFactory 来创建的:

var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
   var builder = builderFactory.CreateBuilder(Server.Features); builder.ApplicationServices = _applicationServices;

IApplicationBuilderFactory 的默认实现 ApplicationBuilderFactory:

public IApplicationBuilder CreateBuilder(IFeatureCollection serverFeatures){    return new ApplicationBuilder(_serviceProvider, serverFeatures);
}

ApplicationBuilder 的实现方式就不在这里多说了,在讲中间件的时候再来细说。

1.4. 配置 IApplicationBuilder

我们比较的熟悉的是在 Startup 类的 Configure 方法中对 IApplicationBuilder 进行配置,其实还有一个 IStartupFilter 也可以用来配置 IApplicationBuilder,并且在 Startup 类的Configure 方法之前执行:

var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
Action<IApplicationBuilder> configure = _startup.Configure;
foreach (var filter in startupFilters.Reverse()) {configure = filter.Configure(configure); }configure(builder);

然后调用 IApplicationBuilder 的 Build 方法,便完成了 RequestDelegate 的创建:

 private RequestDelegate BuildApplication(){...    return builder.Build();
}

2. 启动 Server,监听请求并响应

Server 本身是并不清楚 HttpContext 的细节的,因此它需要接收一个 IHttpApplication 类型的参数,来负责 HttpContext 的创建,由如下定义:

public interface IHttpApplication<TContext>
{   
 TContext CreateContext(IFeatureCollection contextFeatures);  
 Task ProcessRequestAsync(TContext context);    
void DisposeContext(TContext context, Exception exception); }

它的默认实现是 HostingApplication 类,而 ProcessRequestAsync 方法则调用我们上面创建的 RequestDelegate 委托,来完成对 HttpContext 的处理:

public class HostingApplication : IHttpApplication<HostingApplication.Context>
{  
 private readonly RequestDelegate _application;  
 public Task ProcessRequestAsync(Context context)    {    
    return _application(context.HttpContext);} }

最后启动 Server:

var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingApp = new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory);
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);

Server 会绑定一个监听端口,注册HTTP连接事件,最终交给 Http2Stream<TContext> 来处理,通过上面的 hostingApp 来切入到我们的应用程序中,完成整个请求的处理:

public class Http2Stream<TContext> : Http2Stream{   
     private readonly IHttpApplication<TContext> _application;  
     public override async Task ProcessRequestAsync()    {...      
        var context = _application.CreateContext(this);    
        try{          
            await _application.ProcessRequestAsync(context);...}      
        finally{_application.DisposeContext(context, _applicationException);...}...} }

3. 启动 HostedService

HostedService 为我们提供一个注册后台运行服务的机会,它会在随着我们的 ASP.NET Core 程序启动而启动,并在 ASP.NET Core 停止时进行优雅的关闭,有如下定义:

public interface IHostedService{   
  Task StartAsync(CancellationToken cancellationToken);
  Task StopAsync(CancellationToken cancellationToken); }

而它是通过 HostedServiceExecutor 来执行的:

public class HostedServiceExecutor{  
  private readonly IEnumerable<IHostedService> _services;  
  public async Task StartAsync(CancellationToken token)    {  
        await ExecuteAsync(service => service.StartAsync(token));}    
        
  public async Task StopAsync(CancellationToken token)    {  
        await ExecuteAsync(service => service.StopAsync(token));}  
        
 private async Task ExecuteAsync(Func<IHostedService, Task> callback)  
 
{        foreach (var service in _services){          
          await callback(service);}} }

WebHost 会调用 HostedServiceExecutor 的 StartAsync ,从而完成对 HostedService 的启动:

_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
_hostedServiceExecutor = _applicationServices.GetRequiredService<HostedServiceExecutor>();// Fire IApplicationLifetime.Started

_applicationLifetime?.NotifyStarted();// Fire IHostedService.Start
await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false);

这里还有对 IApplicationLifetime 启动事件的触发,以后会介绍一下 IApplicationLifetime 的用途。

到此 WebHost 的整个启动过程介绍完毕。

总结

本文粗略地介绍了一下 ASP.NET Core 中 WebHost 创建及启动,它也是 ASP.NET Core 中的宿主,包含 HttpServer 的启动与监听,而其中也涉及到了很多关键点,对我们以后的开发非常有用,由于篇幅有限,下一章再来介绍一些本文没有解释清楚的概念。

参考文章:

Publishing-and-Running-ASPNET-Core-Applications-with-IIS

相关文章: 

  • .NET Core 2.0 正式发布信息汇总

  • .NET Standard 2.0 特性介绍和使用指南

  • .NET Core 2.0 的dll实时更新、https、依赖包变更问题及解决

  • .NET Core 2.0 特性介绍和使用指南

  • Entity Framework Core 2.0 新特性

  • 体验 PHP under .NET Core

  • .NET Core 2.0使用NLog

  • 升级项目到.NET Core 2.0,在Linux上安装Docker,并成功部署

  • 解决Visual Studio For Mac Restore失败的问题

  • ASP.NET Core 2.0 特性介绍和使用指南

  • .Net Core下通过Proxy 模式 使用 WCF

  • .NET Core 2.0 开源Office组件 NPOI

  • ASP.NET Core Razor页面 vs MVC

  • Razor Page–Asp.Net Core 2.0新功能  Razor Page介绍

  • MySql 使用 EF Core 2.0 CodeFirst、DbFirst、数据库迁移(Migration)介绍及示例

  • .NET Core 2.0迁移技巧之web.config配置文件

  • asp.net core MVC 过滤器之ExceptionFilter过滤器(一)

  • ASP.NET Core 使用Cookie验证身份

  • ASP.NET Core MVC – Tag Helpers 介绍

  • ASP.NET Core MVC – Caching Tag Helpers

  • ASP.NET Core MVC – Form Tag Helpers

  • ASP.NET Core MVC – 自定义 Tag Helpers

  • ASP.NET Core MVC – Tag Helper 组件

原文地址:http://www.cnblogs.com/RainingNight/p/hosting-in-asp-net-core.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323817.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

公众号文章

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。 常用的几个sql整理 1.按照日期统计今天的客流信息。 SELECT * FROM base_disanfang WHERE DATE_FORMAT(create_time,%Y-%m-%d) DATE_FORMAT(NOW(),%Y-%m-%d) 其中base_disanfang为表名…

2017(深圳) .NET技术分享交流会 第二期,将有网络直播

2017 .NET技术分享交流会第一期已在5月13日成功举办&#xff0c;但是有同学反馈哪个地方有点偏&#xff0c;又过去了3个月&#xff0c;这期间一直没找到合适的地方举办活动&#xff0c;一直在南山科技园寻找经济适合的场地&#xff0c;终于找到一个安静&#xff0c;风景好的深圳…

整理几个常用的sql和其他代码

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”常用的几个sql整理1.按照日期统计今天的客流信息。SELECT * FROM base_disanfang WHERE DATE_FORMAT(create_time,%Y-%m-%d) DATE_FORMAT(NOW(),%Y-%m-%d)其中base_disanfang为表名&…

汇编语言(三十三)之四进制转十进制

输入四进制的数&#xff0c;转为十进制输出 程序运行&#xff1a; 代码&#xff1a; datas segmentN_string_max_length db 0ffhN_string db 0, 100h dup(?)N dw 0,0 sum dd 0 input db inp…

Orleans解决并发之痛(二):Grain状态

Grains是Orleans应用程序的构建块&#xff0c;它们是彼此孤立的原子单位&#xff0c;分布的&#xff0c;持久的&#xff0c; 一个典型的Grain是有状态和行为的一个单实例&#xff0c;每个Grain实例的在单线程内执行&#xff0c;Grain之间共享数据通过消息传递&#xff0c;Grain…

汇编语言(三十四)之输出中文

输出中文 程序运行&#xff1a; 代码&#xff1a; daones segmentfull_name db 0,1,2,3,4,5 full_name_length dw $-full_name start_char db 0 change_char_count dw 6color db 1 x dw 40 y …

vue使用element ui实现下拉列表分页的功能!!!

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”前几天做了个功能&#xff0c;需求是使用利用element ui如何给下拉列表分页&#xff0c;经过网上查找&#xff0c;自己摸索&#xff0c;已经完成&#xff0c;今天来记录一下吧。实现的…

ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍

在上一章ASP.NET Core 运行原理解剖[1]:Hosting中&#xff0c;我们介绍了 ASP.NET Core 的启动过程&#xff0c;主要是对 WebHost 源码的探索。而本文则是对上文的一个补充&#xff0c;更加偏向于实战&#xff0c;详细的介绍一下我们在实际开发中需要对 Hosting 做一些配置时经…

汇编语言(三十五)之输入字符串以$结束然后输出字母个数

输入字符串以$结束然后输出字母个数 程序运行&#xff1a; 代码&#xff1a; datas segment buff db 100h dup(?)letter_count dw 0nextline db 0dh,0ah,$datas ends codes segment assume cs:codes,ds:datas main proc far push dsmov ax,0push ax mov ax,datasmov ds,ax…

vue中如何使用vi-for限制遍历的条数?只查询前三条、查询4-6条怎么实现?

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”前言今天整理个简单的功能&#xff0c;vue中的v-for如何限制遍历输出的数据&#xff0c;比如我想在一个存放10条数据的集合中只输出3条怎么写&#xff1f;只想从第四条开始输出到第10条…

编译原理(一)之词法分析

词法分析 (1)参考附录1设计一个简单语言的词法分析程序&#xff0c;要求能够处理注释、换行回车、部分复合运算符&#xff08;如>&#xff09;。 (2)设计并实现含多条简单赋值语句的语法分析程序&#xff0c;要求有一定的出错提示与错误恢复功能。 (参考附录2) 附录1:…

粗略使用.NetCore2.0自带授权登陆Authorize

上篇Linux.NetCoreNginx搭建集群 有朋友提及到如果nginx做集群后应该还会有下一篇文章主讲session控制&#xff0c;一般来说就是登陆&#xff1b;本篇分享的内容不是关于分布式session内容&#xff0c;而是netcore自带的授权Authorize&#xff0c;Authorize粗略的用法&#xff…

vue中如何在地图中标点…

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”前言昨天分享了下vue中v-for的一些特殊用法&#xff0c;料想标题给写成了vi-for…太粗心了。文章连接在这里&#xff1a;vue中如何使用v-for限制遍历的条数&#xff1f;只查询前三条、…

被黑客盯上了…数据都给打包带走了…

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂。”前言在创建数据库的时候&#xff0c;突然之间&#xff0c;发现创建的表通过select * from 表名 查询不到了&#xff0c;于是就开始检查是不是sql语句写错了&#xff0c;检查半天&#…

编译原理(二)之语法分析

采用实验1的简单语言&#xff0c;设计并实现含多条简单赋值语句的语法分析程序&#xff0c;要求采用算符优先的分析算法。 注意与实验1、2的衔接。 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Syste…

ASP.Net Core Razor 页面路由

在服务器端 Web 应用程序框架中&#xff0c;其中非常重要的设计是开发人员如何将URL与服务器上的资源进行匹配&#xff0c;以便正确的处理请求。最简单的方法是将 URL 映射到磁盘上的物理文件&#xff0c;在 Razor 页面框架中&#xff0c;ASP.NET团队就是这样实现的。 关于 Ra…

vue实现下拉列表远程搜索示例(根据关键词模糊搜索)

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言昨天的链接没有放上去……大家访问新站的时候&#xff0c;可以在浏览器地址栏中输入&#xff1a;www.穆雄雄.com或者www.muxiongxiong.cn都可以。今天分享的效果如下&#xff1a;ima…

编译原理(三)之语义分析

采用实验1的简单语言&#xff0c;设计并实现含多条简单赋值语句的语法语义分析程序&#xff0c;要求采用递归下降翻译法。 注意与实验1、2的衔接。 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.T…

活动: 北京Xamarin分享会第6期(2017年9月9日)

每月第二个周六&#xff0c;北京丹棱街5号微软大厦&#xff0c;有什么活动&#xff1f;对, BXUG线下分享活动又来啦! 本次分享嘉宾阵容庞大&#xff0c;在金秋凉爽的季节&#xff0c;期待与大家面对面的交流。内容预告&#xff1a; 案例分享&#xff1a;某大型国企IT项目如何采…

捡到东西说给钱才给东西?算不算敲诈勒索……

“大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号&#xff1a;雄雄的小课堂”前言前两天&#xff0c;将一个那天急要但是后来就不重要的东西&#xff0c;放在车筐里面&#xff0c;结果到目的地一看&#xff0c;没了……椅子还没坐热&#xff0c;有人打来电话了“你…