这么多年一直从事桌面开发,一直没有时间好好学学 web 开发。感觉自己就像从石器时代走来的古代类人猿。由于工作的调整,现在终于有时间学习一下 Web 开发。出于对技术和框架的熟悉和继承,决定还是学习微软的 Web 开发框架(虽然我一直认为java 是一种比C# 更优秀的语言,社区的活力远高于 C#,想想 eclipse 还是算了吧)。
微软的 Web 开发框架从 ASP,ASP.NET,ASP.NET MVC 一直到现在的 ASP.NET Core一路走来,坏消息是没有一个熟悉的,好消息是 ASP.NET Core 是完全开源的,这对学习有很大的帮助。https://github.com/aspnet/AspNetCore 学习技术框架最好的方法是分析源码,但这种方法显然不适合我这种对 Web 开发一窍不通的人,对于我来说,最好的方式是通过项目开发尽快的熟悉 ASP.NET Core 。所以我决定尝试通过开发一个虚拟的项目来熟悉ASP.NET Core,同时因为要从桌面转到 web 开发可以预料到各种问题会接踵而至。我决定重开博客,用一系列文章记录其中曲折的过程以及问题和思考,希望这一系列文章能够对需要重 winform 转型到 web 开发的朋友能够带来一些启发。关于这个虚拟项目的背景我会在下一篇介绍、首先我们要了解一下 ASP.NET Core。
关于 ASP.NET Core 的介绍,网上资料铺天盖地,我就不啰嗦了。在这里主要谈谈从一个桌面 程序员的视角看 ASP.NET Core 的几点感受。
首先用 VS 创建一个 ASP.NET Core 项目,在项目模板选项中因为计算机环境的原因,暂时不选择启用 Docker 支持,Docker 支持在后期可以很方便的添加,当然前提是你搞定了 Docker 的安装和配置,Docker 是个好东西,在你的应用需要跨平台部署时。当然,如果你决定使用 IIS 托管,并且部署环境很固定的话,是不需要 Docker 的。关于 Docker 的安装和配置,网上资料很多,步骤也不是很繁琐,有兴趣的朋友可以百度一下。身份认证选择“个人用户账户”。
当然,在真实的项目中,你可能不太会直接用 新建 的方式创建一个项目,并把所有代码包含在一个项目中。更好的方式是创建一个解决方案,然后创建不同的项目对应程序不同的层。更常用的方式是在解决方案根目录下创建文件夹,把项目放在src文件夹,把测试项目放在 test 文件夹。在程序的分层架构方面 winform 或者说桌面程序和 Web 程序没有太大的区别。两者的区别在于表示层,桌面程序你需要自己设计 UI 程序 ,而 Web 程序你不需要自己设计 UI 程序(感谢各种浏览器),你需要在表示层设计UI的内容,在运行时通过各种框架技术,把他们推送到客户的浏览器中。当然早桌面程序中你也可以这么做,先开发一个通过的窗体作为容器,能够运行用某种文件描述的可编辑的内容,用于工业控制的组态软件通常这么做,我以前做的一个生产线控制系统也是这么做的。
好了,废话少说,我们先看看创创建的项目的文件结构
可以看出,程序的结果简单清晰,应该符合 Rails 约定,模型,视图和控制器的文件夹,到我们真正使用时再讨论,现在我们看看感兴趣的文件 – Program 和 Startup,看得出 ASP.NET Core 已经抛弃了 ASP.NET 把程序编译成 DLL ,运行时由 IIS 托管的方式。ASP.NET core 程序是由 Kestrel 托管的,在部署在 IIS 上时。IIS 仅起到 反向代理的作用,IIS 会把请求转发到 Kestrel ,为此IIS 必须添加 Asp Net core Module 的模块来实现转发功能,以前在做一个用 ASP.Net Core API 宿主的 SIgnalR 程序时,因为对程序启动方式不了解,就碰到这个坑。让我们先看看 Program 的代码
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
启动代码很简练,使用 WebHost 获取 IWebHost 的构建工厂,再构建出 IWebHost 的实例,然后在运行这个实例。让我们感兴趣的构建 IWebHost 时做了哪些事。这是需要打开 ASP.NET Core 的源码一窥究竟。首先打开 WebHostBuilder 文件,在 Hosting 项目中,我们从里面找到了 Builder 方法,代码比较长,这里只截取我们感兴趣的代码,有兴趣的朋友可以自行查看源码
/// <summary>
/// Builds the required services and an <see cref="IWebHost"/> which hosts a web application.
/// </summary>
public IWebHost Build()
{
.....
var hostingServices = BuildCommonServices(out var hostingStartupErrors);
.....
AddApplicationServices(applicationServices, hostingServiceProvider);
IServiceProvider GetProviderFromFactory(IServiceCollection collection)
{
var provider = collection.BuildServiceProvider();
var factory = provider.GetService<IServiceProviderFactory<IServiceCollection>>();
if (factory != null && !(factory is DefaultServiceProviderFactory))
{
using (provider)
{
return factory.CreateServiceProvider(factory.CreateBuilder(collection));
}
}
return provider;
}
}
private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors)
{
...
var services = new ServiceCollection();
services.AddSingleton(_options);
services.AddSingleton<IWebHostEnvironment>(_hostingEnvironment);
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<AspNetCore.Hosting.IHostingEnvironment>(_hostingEnvironment);
services.AddSingleton<Extensions.Hosting.IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton(_context);
var builder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath)
.AddConfiguration(_config, shouldDisposeConfiguration: true);
_configureAppConfigurationBuilder?.Invoke(_context, builder);
var configuration = builder.Build();
// register configuration as factory to make it dispose with the service provider
services.AddSingleton<IConfiguration>(_ => configuration);
_context.Configuration = configuration;
var listener = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticListener>(listener);
services.AddSingleton<DiagnosticSource>(listener);
services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>();
services.AddTransient<IHttpContextFactory, DefaultHttpContextFactory>();
services.AddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.AddOptions();
services.AddLogging();
services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();
return services;
}
在 Builder 方法中调用 BuildCommonServices 创建并返回一个 IServiceCollection 的实例, ASp.NET Core 使用这个容器实现依赖注入,并把基础服务通过 AddXXX<T>方法注入到容器。使用不同的方法签名可以控制注入的对象声明周期。以前做桌面程序时,也需要很多基础的服务,一般情况下也会用到容器,然后把它基础在程序的运行时单例中,在 Program 中,在启动主窗体前先把服务注入到运行时,这样就可以在程序任意地方通过接口获取这些服务,类似于 GetService<T>();这是一种手动的依赖注入方式,类似 Addin 方式,在使用服务时需要手动通过接口获取,功能远没有 ASP.NET core 的依赖注入强大.不过应付一般的桌面程序也就够了,毕竟咱也编不了多大的程序。
让我再回到 Program 中看第二句 UseStartup<Startup>() ,UseStartUp 是一个扩展方法,定义在 WebHostBuilderExtensions 文件中 ,通过代码我们可以知道这个方法就是把 StartUp 的实例注入到 IOC 容器中,只不过代码里做了些判断,注入的对象是否实现了 IStartup 接口。StartUp 代码如下
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
ASP.NET Core 使用了中间件实现对请求管道的配置,在 StarUp 中你可以添加自己的中间件实现特殊的功能,这点类似 WCF 的上下文拦截技术(在我原来的博客里写过.NET基于上下文的拦截,不过后来博客密码忘了,只好重新创建一个),可以在请求管道中增加自己需要的垂直功能(如日志记录,身份验证 - 这两项 ASP.NETCore 都有,而且做的不错)- 就是所谓的面向切面的编程 AOP,在 StartUp 中还可以在 IOC 容器中注入依赖。修改甚至加载自己的配置项。必须入,下面这段代码实现了用户数据存储的配置和依赖注入。
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
现在我们通过几段简单的源码,简单了解了 ASP.NET Core 的启动过程,虽然很简单,单让我们窥视到了 ASP.NET Core 的基础-依赖注入,这应该是微软和以前不一样的地方。ASP.NET Core 的其他技术在虚拟项目开始用到的地方在继续写。
下一篇计划暂时离开 ASP.NETCore 介绍一下项目的背景和这个项目的领域模型.,虚拟项目已经托管在 GitHub ,准备持续开发,敢兴趣的朋友可以 Fork me https://github.com/wangxiyou/ProductionLBS
PS:一直为这个系列博客起名而发愁,突然想到以前的经典书名 21 天 学会 XXX ,借来一用,窃书为雅,窃书名更雅,感觉 21 天学会太难,于是改成 从桌面到 Web - 二十几天学 ASP.NETCore
来源:oschina
链接:https://my.oschina.net/u/4354006/blog/3322888