C# · #csharp#dotnet-core#migration

.NET Core 2.0 迁移实战:从 1.x 到 2.0

2017.10.30 C# 3 min 1.3k
// 目录 · contents

2017 年 8 月 .NET Core 2.0 发布,这次升级的改动比预期大,但都是往好的方向走。我们趁这次机会把公司内部的一个 API 服务从 1.1 迁移到了 2.0,这篇文章记录整个过程和踩到的坑。

1. 项目文件格式变化

1.x 用 project.json,2.0 换回了 .csproj 格式(但是简化版):

1
2
3
4
5
6
7
8
9
10
<!-- .NET Core 2.0 的 .csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
</Project>

注意 Microsoft.AspNetCore.All 这个元包,它包含了 ASP.NET Core 生态的所有包,不需要像 1.x 那样一个个添加。这让 csproj 文件干净了很多。

迁移工具:

1
dotnet migrate  # 将 project.json 自动转换为 .csproj

大多数情况下自动迁移可以工作,但有些自定义构建步骤需要手动处理。

2. Program.cs 简化

1.x 的 WebHostBuilder 需要手动配置很多选项,2.0 引入了 WebHost.CreateDefaultBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1.x 写法
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile("appsettings.json");
config.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true);
config.AddEnvironmentVariables();
})
.ConfigureLogging(logging =>
{
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>()
.Build();

host.Run();
}

// 2.0 写法(上面的配置全部内置了)
public static void Main(string[] args)
{
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.Run();
}

CreateDefaultBuilder 默认已经配置了:Kestrel、IIS 集成、从 appsettings.json 和环境变量读取配置、Console/Debug 日志。

3. Startup 构造函数简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1.x:手动注入 IHostingEnvironment 和 IConfiguration
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
Configuration = builder.Build();
}

public IConfiguration Configuration { get; }
}

// 2.0:直接注入 IConfiguration(由 CreateDefaultBuilder 构建好的)
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }
}

4. Authentication 中间件变化

这是迁移里改动最大的地方,1.x 的 app.UseJwtBearerAuthentication() 等方法废弃了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1.x 写法
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Authority = "https://auth.example.com",
Audience = "my-api"
});

// 2.0 写法:认证配置移到 ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://auth.example.com";
options.Audience = "my-api";
});
}

public void Configure(IApplicationBuilder app)
{
app.UseAuthentication(); // 中间件只需要这一行
app.UseMvc();
}

这个改动破坏性比较大,迁移时需要逐一检查所有认证配置。

5. Entity Framework Core 2.0 改进

EF Core 2.0 补上了一些 1.x 里缺失的关键功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 2.0 新增:全局查询过滤器(软删除神器)
modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsDeleted);
// 之后所有查询自动加上 WHERE IsDeleted = 0,无需手动添加

// 需要包含已删除数据时
db.Users.IgnoreQueryFilters().Where(u => u.Name == "Alice");

// 2.0 新增:拥有类型(Owned Types)
modelBuilder.Entity<Order>()
.OwnsOne(o => o.ShippingAddress); // Address 没有独立的表

// 2.0 改进:更好的 Include 支持
var orders = db.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ThenInclude(p => p.Category)
.ToList();

踩坑记录

迁移过程中最耗时的问题是 IHttpContextAccessor 线程安全问题

我们有一个中间件,从 HttpContext 读取当前用户信息并存到一个 Singleton 服务里,供后续业务逻辑使用。1.x 时没问题,迁移到 2.0 后偶发出现 A 请求读到了 B 请求的用户信息。

原因:2.0 改进了异步执行模型,某些情况下 AsyncLocalHttpContext 内部依赖的机制)的传播行为有变化。

解决方案:把这个 Singleton 服务改成 Scoped,通过构造函数注入 IHttpContextAccessor,而不是手动存储用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 错误做法
public class CurrentUserService // Singleton
{
private static AsyncLocal<UserInfo> _current = new AsyncLocal<UserInfo>();
public UserInfo Current => _current.Value;
public void Set(UserInfo user) => _current.Value = user;
}

// 正确做法
public class CurrentUserService // Scoped
{
private readonly IHttpContextAccessor _accessor;

public CurrentUserService(IHttpContextAccessor accessor)
{
_accessor = accessor;
}

public string UserId =>
_accessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}

总结

.NET Core 2.0 是个显著的成熟版本:

  • API 覆盖率大幅提升:.NET Standard 2.0 兼容了约 32000 个 API,大量 .NET Framework 包可以直接使用
  • 项目文件更简洁csproj + Microsoft.AspNetCore.All 元包
  • CreateDefaultBuilder 减少了大量样板代码
  • Authentication 中间件统一化,配置更清晰但有 breaking change

如果你在 1.x 上犹豫要不要升级,2.0 是一个值得迁移的版本。我们迁移完成后,新功能开发速度明显提升,运维反映部署也更稳定了。

作者 · authorzt
发布 · date2017-10-30
篇幅 · length1.3k 字 · 3 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论