TeamsApp升级之路 之 EF Core SQL 篇
我们继续 LuckyDraw 的升级之路,上篇讲了在infra层面的数据库配置,这篇文章主要介绍如何在 LuckyDraw 的 bot api service 里使用 EF core 来处理抽奖,并且同时保证向前兼容,也就是说如果已经有个抽奖是使用 Azure storage table 的,那就继续使用 storage table,虽然这种做法增加了大量的代码工作量,但是可以保证用户体验,不会升级之后,用户突然觉的之前的抽奖怎么打不开了或者不能参与了。
整个升级过程中,我深深体会到 test 代码的作用,几年前花了大量的时间写的测试代码,现在来看非常值得。如果没有这些代码,我根本就不敢修改当前的代码,因为无法预知改了之后会不会影响之前的逻辑。
好,我们来看看升级中的几个要点:
1. 使用 Managed Identify 来访问数据库
传统的数据库访问用户名和密码,这样的问题是密码会出现在 connection string 里,或者出现在某个配置里,这样带来了一些安全隐患,azure 为我们提供了一个更加安全的方式,Managed Identity,具体的配置方法可以参考微软的官方文档:https://learn.microsoft.com/en-us/azure/azure-functions/functions-identity-access-azure-sql-with-managed-identity
所以在代码层面,我们可以这么做:
public static void UseManagedIdentity(this DbContext dbContext)
{
if (dbContext.Database.ProviderName != "Microsoft.EntityFrameworkCore.SqlServer")
{
return;
}
var tokenProvider = new AzureServiceTokenProvider();
var conn = (SqlConnection)dbContext.Database.GetDbConnection();
conn.AccessToken = tokenProvider.GetAccessTokenAsync("https://database.windows.net/").Result;
}
public class LuckyDrawDbContext : DbContext
{
public LuckyDrawDbContext(DbContextOptions<LuckyDrawDbContext> options) : base(options)
{
this.UseManagedIdentity();
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
}
上面的代码实际上是先使用 AzureServiceTokenProvider
来向 azure 获取一个 access token,然后数据库的访问就用这个 token 来访问。
值得一提的是我这里使用了 NoTracking 模式,来防止 EF Core 默认的 tracking 行为。提高执行效率。
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
2. 日期类型
Teams SDK 很多地方(几乎所有地方)都是使用 DateTimeOffset 来传递日期,这个类型的好处是里面带了 timezone (offset) 信息。SQL server 实际上也支持这种类型。但是我在 LuckyDraw 这次数据库设计中还是使用了传统的 DateTime datetime2(7)
类型,原因是我有另外一个数据库字段来记录 timezone 信息,在DateTime里保存全部都是 UTC,这样不会混乱。我相信很多人不认同这种做法,但是我自己多年的经验告诉我,time一旦涉及到 timezone 非常的复杂,很多问题很难调试,所以我尽量的简化,一旦接收到 date time 信息,我第一时间转换成 UTC。
在需要 DateTimeOffset 类型的地方,我可以来回转换:
- DateTime 转 DateTimeOffset:
new DateTimeOffset(dateTime, TimeSpan.Zero)
- DateTimeOffset 转 DateTime:
dateTimeOffset.UtcDateTime
3. 数据库版本管理
数据库版本管理是一个比较复杂的话题,并没有太好的解决方法,我目前使用的是 DbUp。我给我的数据库脚本编好号码,然后 embed 到我的project里:
<ItemGroup>
<EmbeddedResource Include="Database/DbScripts/*.sql" LogicalName="DatabaseScripts.%(Filename)%(Extension)" />
</ItemGroup>
然后启动的时候,使用 DbUp 升级数据库,确保数据库里的schema是最新的额版本:
var upgradeEngine = DeployChanges.To.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(currentAssembly, path => path.Contains("DatabaseScripts."))
.WithTransactionPerScript()
.LogScriptOutput()
.LogToConsole()
.Build();
if (!upgradeEngine.IsUpgradeRequired())
{
LogInformation("Database upgrade is not required.");
return;
}
var result = upgradeEngine.PerformUpgrade();
if (!result.Successful)
{
throw new InvalidOperationException("DbUp failed to perform upgrade.", result.Error);
}
4. 使用 in-memory database 进行自动化测试
有一些朋友喜欢使用真是的 SQL server 进行测试,因为这样能够cover所有的真实的场景,我个人比较喜欢使用 EF Core的 in-memory database 来进行测试,因为它的执行速度非常快,当我们的项目有几百个测试用例的时候,如果使用真实的 SQL server 测试,跑一遍用例起码要 5-10 分钟,但是如果是 in-memory database,通常可以在一分钟内完成,这个对开发效率来说有很大提高,所以我还是喜欢 in-memory db。
当然 in-memory db 有一些问题,比如发现不了 FK 冲突,无法做性能测试。等等。
在测试代码里,我替换掉 DbContext 的option。替换成 in-memory db 的option。
services.ReplaceScoped(GetInMemoryOptions<LuckyDrawDbContext>(mainDbName));
public static IServiceCollection ReplaceScoped<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class
{
var service = services.First(s => s.ServiceType == typeof(TService));
services.Remove(service);
return services.AddScoped(implementationFactory);
}
public static Func<IServiceProvider, DbContextOptions<T>> GetInMemoryOptions<T>(string dbName) where T : DbContext
{
return (_) =>
{
var builder = new DbContextOptionsBuilder<T>();
builder.UseInMemoryDatabase(databaseName: dbName);
return builder.Options;
};
}
希望这些点能够给大家升级 teams app 的时候有一些参考和启发。