深度优先

这个家伙好懒,除了文章什么都没留下

0%

作者:码农阿宇
出处:https://www.cnblogs.com/CoderAyu/p/10242230.html

背景

前些天公司服务器数据库访问量偏高,运维人员收到告警推送,安排我团队小伙伴排查原因.

我们发现原来系统定期会跑一个回归测试,该测运行的任务较多,每处理一条任务都会到数据库中取相关数据,高速地回归测试也带来了高频率的数据库读取.

解决方案1

我们认为每个任务要取的数据大相径庭,因此我们考虑对这个过程进行修改,加入MemoryCache把数据库中读取到的数据进行缓存.

整个修改非常简单,相信对常年混迹在博客园中的各位大佬来说小菜一碟,因此小弟不再叙述添加缓存的步骤细节.

从缓存的添加,代码提交,Teamcity 编译通过,到测试环境,QA环境的安装无比流畅,一切显得如手到擒来.

嗯,优秀是一种习惯, 没有一点办法.

人生如戏,当我们还沉浸在”我加的Cache不可能又BUG”的自信中时,QA传来噩耗,回归测试大量未通过 ….

故障排查

之前习惯了使用Redis缓存,因此,常识告诉我们 — 在数据库中数据没有改动的前提下,加了缓存后读取的数据的效果和从数据库中读取的效果是一模一样的.

除非 ,,, 除非 这个常识是错误的….

因此我们加了日志,对写入缓存前后读取出来的数据进行了对比,结果出人意料.

该死 MemoryCache 毁我老脸,丢我精度,拿命来!!!!!

从日志中看到,第一行是从数据库中读取的结果,第二行是从cache中读取的,前两条数据完全一致,到了第三条,第四条,第五条,仔细观察发现,在小数点后面,居然有些小数点后比较微小的变化,不管变化的大小但数据确实发生改变了,所以MemoryCache会影响数据精度??这样会改变数据精度的MemoryCache又有何用??

机智的我,似乎早已看穿了一切,这肯定不是MenoryCache的锅!!!

不一样的MemoryCache

我从 中扒出了MemoryCache的源码一探究竟.

定位到MemoryCache中的AddOrGetExisting方法,我们看到,其实我们把数据存储到该缓存的过程本质是把该对象存到一个名为_entries的 Hashtable 中,同样,取数据也是通过Key到该Hashtable中取出来,整个过程并没有对该对象进行序列化反序列等,也没有对该对象进行clone操作.这就意味着我们之前存入的,和后面取出的(不管我们从MemoryCache中取数据取多少次),永远只取出同一个对象.

这一点,和我之前使用的RedisCache是有很大区别的.我们在Redis中存入数据,是把对象序列化后存到Redis中,取数据是把Redis中的字节数据反序列成对象,意味着前一次存入的,和后一次取出的,已经不是同一个对象了,因此Redis中的数据是安全的.

猜想

我做出了一个大胆的猜想,之前从MemoryCache中取出来的数据之所以变化了,可能是取出对象后,复杂的处理过程中对该对象进行了什么修改操作,所以后期,再次从数据库中读取数据,读出来的已经已经不是最初存入的数据,而是前一次修改之后的数据.带着这个猜想,我对代码进行了修改.

解决方案2

从MenoryCache中取到数据后对结果进行clone(),这样即使程序对取出来的结果进行了修改也不会影响Cache中的数据了.

又是一次提心掉到的提交,编译,安装后, 回归测试顺利通过.

感觉人生到达了高潮 -_-

关联删除通常是一个数据库术语,用于描述在删除行时允许自动触发删除关联行的特征;即当主表的数据行被删除时,自动将关联表中依赖的数据行进行删除,或者将外键更新为NULL或默认值。

数据库关联删除行为

我们先来看一看SQL Server中支持的行为。在创建外键约束时,可以指定关联表在主表删除行时,对依赖的数据如何执行操作。例如下面的SQL语句,[Order Details]表中[OrderID]字段 是外键,依赖于[Orders]表中的主键[OrderID]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE [Orders] (
[OrderID] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[OrderDate] datetime2 NULL,
CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
);

GO

CREATE TABLE [Order Details] (
[DetailId] int NOT NULL IDENTITY,
[OrderID] int NULL,
[ProductID] int NOT NULL,
CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE SET NULL
);

外键约束[FK_Order Details_Orders_OrderID]末尾的语句是ON DELETE SET NULL,表示当主表的数据行删除时,自动将关联表数据行的外键更新为NULL

在SQL Server中支持如下四种行为:

  • ON DELETE NO ACTION
    默认行为,删除主表数据行时,依赖表中的数据不会执行任何操作,此时会产生错误,并回滚 DELETE语句。例如会产生下面的错误:

    DELETE 语句与 REFERENCE 约束”FK_Order Details_Orders_OrderID”冲突。该冲突发生于数据库”Northwind_Test”,表”dbo.Order Details”, column ‘OrderID’。
    语句已终止。

  • ON DELETE CASCADE
    删除主表数据行时,依赖表的中数据行也会同步删除。

  • ON DELETE SET NULL
    删除主表数据行时,将依赖表中数据行的外键更新为 NULL。为了满足此约束,目标表的外键列必须可为空值。

  • ON DELETE SET DEFAULT
    删除主表数据行时,将依赖表的中数据行的外键更新为默认值。为了满足此约束,目标表的所有外键列必须具有默认值定义;如果外键可为空值,并且未显式设置默认值,则将使用 NULL作为该列的隐式默认值。

简单介绍了数据库中行为后,我们来着重介绍 EF Core 中的关联实体的行为。

定义实体

我们先定义两个实体OrderOrderDetail分别表示订单和订单明细;其中OrderOrderDetail的关系是一对多,在OrderDetail实体中OrderID表示外键,依赖于Order实体中的主键OrderID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Order
{
public int OrderID { get; set; }

public string Name { get; set; }

public DateTime? OrderDate { get; set; }

public ICollection<OrderDetail> OrderDetails { get; set; }
}

public class OrderDetail
{
public int DetailId { get; set; }

public int? OrderID { get; set; }

public int ProductID { get; set; }

public Order Order { get; set; }
}

Fluent API 配置关联实体

DbContextOnModelCreating方法中,我们使用 Fluent API 配置实体中之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NorthwindContext : DbContext
{

public virtual DbSet<Order> Orders { get; set; }
public virtual DbSet<OrderDetail> OrderDetails { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(
builder =>
{
builder.HasMany<OrderDetail>(e => e.OrderDetails).WithOne(e => e.Order).HasForeignKey(e => e.OrderID).OnDelete(DeleteBehavior.ClientSetNull);
});
}
}

OnDelete方法中,需要传递参数DeleteBehavior枚举,分别有如下四个值:

1
2
3
4
5
6
7
8
9
10
public enum DeleteBehavior
{
Cascade,

SetNull,

ClientSetNull,

Restrict
}

这四个枚举值的分别表示不同的行为,这也是我们今天的重点。

创建表结构

我们分别使用使用这这个枚举值,来创建数据表结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
[InlineData(DeleteBehavior.Cascade)]
[InlineData(DeleteBehavior.SetNull)]
[InlineData(DeleteBehavior.ClientSetNull)]
[InlineData(DeleteBehavior.Restrict)]
[Theory]
public void Create_Database(DeleteBehavior behavior)
{
using (var northwindContext = new NorthwindContext(behavior))
{
northwindContext.Database.EnsureDeleted();
northwindContext.Database.EnsureCreated();
}
}

四个枚举值创建表的SQL语句类似如下,唯一区别在于创建外键约束[FK_Order Details_Orders_OrderID]ON DELETE {} 后面的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE [Orders] (
[OrderID] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[OrderDate] datetime2 NULL,
CONSTRAINT [PK_Orders] PRIMARY KEY ([OrderID])
);

GO

CREATE TABLE [Order Details] (
[DetailId] int NOT NULL IDENTITY,
[OrderID] int NOT NULL,
[ProductID] int NOT NULL,
CONSTRAINT [PK_Order Details] PRIMARY KEY ([DetailId]),
CONSTRAINT [FK_Order Details_Orders_OrderID] FOREIGN KEY ([OrderID]) REFERENCES [Orders] ([OrderID]) ON DELETE CASCADE
);

四个枚举值分别对应的SQL语句如下:

枚举值 SQL语句
DeleteBehavior.Cascade ON DELETE CASCADE
DeleteBehavior.SetNull ON DELETE SET NULL
DeleteBehavior.ClientSetNull ON DELETE NO ACTION
DeleteBehavior.Restrict ON DELETE NO ACTION

EF Core 关联实体删除行为

我们分别通过枚举值与是否跟踪关联实体,进行代码测试,测试代码如下:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
[InlineData(DeleteBehavior.Cascade, true)]
[InlineData(DeleteBehavior.Cascade, false)]
[InlineData(DeleteBehavior.SetNull, true)]
[InlineData(DeleteBehavior.SetNull, false)]
[InlineData(DeleteBehavior.ClientSetNull, true)]
[InlineData(DeleteBehavior.ClientSetNull, false)]
[InlineData(DeleteBehavior.Restrict, true)]
[InlineData(DeleteBehavior.Restrict, false)]

[Theory]
public void Execute(DeleteBehavior behavior, bool includeDetail)
{
using (var northwindContext = new NorthwindContext(behavior))
{
northwindContext.Database.EnsureDeleted();
northwindContext.Database.EnsureCreated();
}

int orderId;
int detailId;
using (var northwindContext = new NorthwindContext(behavior))
{
var order = new Order {
Name = "Order1"
};

var orderDetail = new OrderDetail {
ProductID = 11
};
order.OrderDetails = new List<OrderDetail> {
orderDetail
};

northwindContext.Set<Order>().Add(order);
northwindContext.SaveChanges();

orderId = order.OrderID;
detailId = orderDetail.DetailId;
}

using (var northwindContext = new NorthwindContext(behavior))
{
var queryable = northwindContext.Set<Order>().Where(e => e.OrderID == orderId);
if (includeDetail){
queryable = queryable.Include(e => e.OrderDetails);
}

var order = queryable.Single();
northwindContext.Set<Order>().Remove(order);

try
{
northwindContext.SaveChanges();
DumpSql();
}
catch (Exception)
{
DumpSql();
throw;
}

}

using (var northwindContext = new NorthwindContext(behavior))
{
var orderDetail = northwindContext.Set<OrderDetail>().Find(detailId);
if (behavior == DeleteBehavior.Cascade)
{
Assert.Null(orderDetail);
}
else
{
Assert.NotNull(orderDetail);
}
}
}
枚举值 是否跟踪关联实体 是否成功调用SaveChange 关联实体是否存在 执行的SQL
DeleteBehavior.Cascade No 成功 DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.Cascade YES 成功 DELETE FROM [Order Details] WHERE [DetailId] = 1;DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.SetNull No 成功 YES(外键为NULL DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.SetNull YES 成功 YES(外键为NULL UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1;DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.ClientSetNull No 失败(外键约束) YES DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.ClientSetNull YES 成功 YES(外键为NULL UPDATE [Order Details] SET [OrderID] = NULL WHERE [DetailId] = 1;DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.Restrict No 失败(外键约束) YES DELETE FROM [Orders] WHERE [OrderID] = 1
DeleteBehavior.Restrict YES 失败(外键约束) YES DELETE FROM [Orders] WHERE [OrderID] = 1

总结

根据上面的测试结果,我们可以出得如下结论:

DeleteBehavior.Cascade

  • 如果关联实体未被跟踪,主实体的状态标记为删除,执行SaveChage时,在删除主表的数据的同时,通过数据库的行为删除关联表的数据行;
  • 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的状态也会标记为删除,执行SaveChange时,先删除关联表的数据行,然后再删除主表的数据行;
  • 外键可以设置非空值、也可以设置为可为空值;
  • 关联实体可以不被跟踪。

DeleteBehavior.SetNull

  • 如果关联实体未被跟踪,主实体的状态标记为删除,执行SaveChage时,在删除主表的数据时,通过数据库的行为将关联表数据行的外键更新为NULL,;
  • 如果关联实体已经被跟踪,将主实体的状态标记为删除时,关联实体的外键会被设置为null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行 ,然后删除主表的数据行;
  • 因为要将外键更新为NULL,所以外键必须设置为可空字段;
  • 关联实体可以不被跟踪。

DeleteBehavior.ClientSetNull

  • 数据库不会执行任何行为;
  • 关联实体必须被跟踪,将主实体的状态标记为删除时,关联实体的外键被设置为null,同时将关联实体的状态标记为修改,执行SaveChange时,先更新关联表的数据行,然后删除主表的数据行(此时的行为与DeleteBehavior.SetNull一致);
  • 因为要将外键更新为NULL,所以外键必须设置为可空字段;
  • 关联实体必须被跟踪,否则保存数据时会抛出异常。

DeleteBehavior.Restrict

  • 框架不执行任何操作,由开发人员决定关联实体的行为,可以将关联实体的状态设置为删除,也可以将关联实体的外键设置为null
  • 因为要修改关联实体的状态或外键的值,所以关联实体必须被跟踪。

本文为Google翻译的 原文地址

本文介绍如何使用SQL数据库在ASP.NET Core中使用SQL本地化。 演示中的SQL本地化使用Entity Framework Core访问SQLite数据库。 可以将其配置为使用任何EF核心提供程序。

使用SQL本地化

要使用SQL本地化,需要将该库添加到project.json文件中的依赖项。 该库称为Localization.SqlLocalizer。

1
2
3
"dependencies": {
"Localization.SqlLocalizer": "2.0.0"
}

然后可以将其添加到Startup类的ConfigureServices方法中。 AddSqlLocalization方法要求在实体框架服务扩展中配置LocalizationModelContext类。 在此示例中,SQLite用作LocalizationModelContext上下文类的提供程序。 其余的本地化可以根据需要进行配置。 该示例支持en-US,de-CH,fr-CH,it-CH。

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
32
33
34
35
36
37
38
39
40
public void ConfigureServices(IServiceCollection services)
{
// services.AddLocalization(options => options.ResourcesPath = "Resources");

// init database for localization
var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

services.AddDbContext<LocalizationModelContext>(options =>
options.UseSqlite(
sqlConnectionString,
b => b.MigrationsAssembly("AspNet5Localization")
)
);

// Requires that LocalizationModelContext is defined
// services.AddSqlLocalization(options => options.UseTypeFullNames = true);
services.AddSqlLocalization();

services.AddMvc()
.AddViewLocalization()
.AddDataAnnotationsLocalization();

services.AddScoped<LanguageActionFilter>();

services.Configure<RequestLocalizationOptions>(
options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("de-CH"),
new CultureInfo("fr-CH"),
new CultureInfo("it-CH")
};

options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
}

还需要在Configure方法中添加本地化。

1
2
3
4
5
6
7
8
9
10
11
12
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
loggerFactory.AddDebug();

var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);

app.UseStaticFiles();

app.UseMvc();
}

现在需要创建数据库。 如果使用SQLite,则在 SqliteCreateLocalizationRecord.sql 文件中 提供创建sql脚本 。 如果使用其他数据库,则需要创建该数据库。 我没有提供迁移脚本。 可以使用SQLite Manager在Firefox中执行SQLite脚本。

1
2
3
4
5
6
7
8
CREATE TABLE "LocalizationRecords" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_DataEventRecord" PRIMARY KEY AUTOINCREMENT,
"Key" TEXT,
"ResourceKey" TEXT,
"Text" TEXT,
"LocalizationCulture" TEXT,
"UpdatedTimestamp" TEXT NOT NULL
)

该数据库现在应该存在。 我添加了一些基本的演示行。

SQL Localization的默认配置使用资源名称,然后使用资源密钥(如果遵循Microsoft的建议,则可能是默认语言文本),然后使用区域性。 这些属性中的每个属性在数据库中都有一个单独的字段。 如果找不到本地化,则返回搜索到的关键字。 这是一个未找到并返回到UI的本地化示例。

ASP.NET Core MVC控制器:

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
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace AspNet5Localization.Controllers
{

[ServiceFilter(typeof(LanguageActionFilter))]
[Route("api/{culture}/[controller]")]
public class AboutWithCultureInRouteController : Controller
{
// http://localhost:5000/api/it-CH/AboutWithCultureInRoute
// http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

private readonly IStringLocalizer<SharedResource> _localizer;


public AboutWithCultureInRouteController(IStringLocalizer<SharedResource> localizer)
{
_localizer = localizer;
}

[HttpGet]
public string Get()
{
return _localizer["Name"];
}
}
}

使用的网址:

1
http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

结果:ResoureKey:SharedResource,Key:名称,本地化文化:fr-CH

1
SharedResource.Name.fr-CH

这应该使查找和添加缺失的本地化变得容易。

配置SQL本地化

还可以将SQL Localization配置为使用不同的键在数据库中搜索本地化。 可以使用services.AddSqlLocalization并添加options参数在Startup类中进行配置。

SqlLocalizationOptions具有两个属性,UseTypeFullNames和UseOnlyPropertyNames。 如果UseOnlyPropertyNames为true,则在数据库中仅将属性名称用作带有ResourceKey全局键的键。 您还可以通过设置UseTypeFullNames将其配置为使用FullNames作为键。 如果设置了此名称,则数据库的ResourceKey属性中需要完整的类型名称。

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlLocalizationOptions
{
/// <summary>
/// If UseOnlyPropertyNames is false, this property can be used to define keys with full type names or just the name of the class
/// </summary>
public bool UseTypeFullNames { get; set; }

/// <summary>
/// This can be used to use only property names to find the keys
/// </summary>
public bool UseOnlyPropertyNames { get; set; }
}

在Startup类中使用选项的示例:

1
2
3
4
5
6
7
8
9
var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

services.AddEntityFramework()
.AddSqlite()
.AddDbContext<LocalizationModelContext>(
options => options.UseSqlite(sqlConnectionString));

// Requires that LocalizationModelContext is defined
services.AddSqlLocalization(options => options.UseTypeFullNames = true);

用于HTTP请求的Controller:

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
using System.Globalization;
using System.Threading;

namespace AspNet5Localization.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;

[Route("api/[controller]")]
public class AboutController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
private readonly IStringLocalizer<AboutController> _aboutLocalizerizer;

public AboutController(IStringLocalizer<SharedResource> localizer, IStringLocalizer<AboutController> aboutLocalizerizer)
{
_localizer = localizer;
_aboutLocalizerizer = aboutLocalizerizer;
}

[HttpGet]
public string Get()
{
// _localizer["Name"]
return _aboutLocalizerizer["AboutTitle"];
}
}
}

网址:

1
http://localhost:5000/api/about?culture=it-CH

结果:从结果中可以看到,SQL本地化使用FullName搜索了本地化。 ResoureKey:AspNet5Localization.Controllers.AboutController,键:AboutTitle,LocalizationCulture:it-CH

1
AspNet5Localization.Controllers.AboutController.AboutTitle.it-CH

SQL本地化详细

SQL本地化库使用扩展方法来提供其服务,该服务可在应用程序的Startup类中使用。 该库取决于Entity Framework Core。 任何数据库提供程序都可以用于此目的。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;

namespace Microsoft.Extensions.DependencyInjection
{
using global::Localization.SqlLocalizer;
using global::Localization.SqlLocalizer.DbStringLocalizer;

/// <summary>
/// Extension methods for adding localization servics to the DI container.
/// </summary>
public static class SqlLocalizationServiceCollectionExtensions
{
/// <summary>
/// Adds services required for application localization.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddSqlLocalization(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

return AddSqlLocalization(services, setupAction: null);
}

/// <summary>
/// Adds services required for application localization.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="setupAction">An action to configure the <see cref="LocalizationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddSqlLocalization(
this IServiceCollection services,
Action<SqlLocalizationOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizerFactory),
typeof(SqlStringLocalizerFactory),
ServiceLifetime.Singleton));
services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizer),
typeof(SqlStringLocalizer),
ServiceLifetime.Singleton));

if (setupAction != null)
{
services.Configure(setupAction);
}
return services;
}
}
}

SqlStringLocalizerFactory类实现IStringLocalizerFactory,该对象负责数据库访问。 该数据库仅用于每个资源的第一个请求。 这意味着此后会有更好的性能,但是新的翻译仅 应用程序重新启动 才能读取 。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.PlatformAbstractions;

public class SqlStringLocalizerFactory : IStringLocalizerFactory
{
private readonly LocalizationModelContext _context;
private readonly ConcurrentDictionary<string, IStringLocalizer> _resourceLocalizations = new ConcurrentDictionary<string, IStringLocalizer>();
private readonly IOptions<SqlLocalizationOptions> _options;
private const string Global = "global";

public SqlStringLocalizerFactory(
LocalizationModelContext context,
IApplicationEnvironment applicationEnvironment,
IOptions<SqlLocalizationOptions> localizationOptions)
{
if (context == null)
{
throw new ArgumentNullException(nameof(LocalizationModelContext));
}

if (applicationEnvironment == null)
{
throw new ArgumentNullException(nameof(applicationEnvironment));
}

if (localizationOptions == null)
{
throw new ArgumentNullException(nameof(localizationOptions));
}

_options = localizationOptions;
_context = context;
}

public IStringLocalizer Create(Type resourceSource)
{
SqlStringLocalizer sqlStringLocalizer;

if (_options.Value.UseOnlyPropertyNames)
{
if (_resourceLocalizations.Keys.Contains(Global))
{
return _resourceLocalizations[Global];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(Global), Global);
return _resourceLocalizations.GetOrAdd(Global, sqlStringLocalizer);

}

if (_options.Value.UseTypeFullNames)
{
if (_resourceLocalizations.Keys.Contains(resourceSource.FullName))
{
return _resourceLocalizations[resourceSource.FullName];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.FullName), resourceSource.FullName);
_resourceLocalizations.GetOrAdd(resourceSource.FullName, sqlStringLocalizer);
}


if (_resourceLocalizations.Keys.Contains(resourceSource.Name))
{
return _resourceLocalizations[resourceSource.Name];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.Name), resourceSource.Name);
return _resourceLocalizations.GetOrAdd(resourceSource.Name, sqlStringLocalizer);
}

public IStringLocalizer Create(string baseName, string location)
{
if (_resourceLocalizations.Keys.Contains(baseName + location))
{
return _resourceLocalizations[baseName + location];
}

var sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(baseName + location), baseName + location);
return _resourceLocalizations.GetOrAdd(baseName + location, sqlStringLocalizer);
}

private Dictionary<string, string> GetAllFromDatabaseForResource(string resourceKey)
{
return _context.LocalizationRecords.Where(data => data.ResourceKey == resourceKey).ToDictionary(kvp => (kvp.Key + "." + kvp.LocalizationCulture), kvp => kvp.Text);
}
}
}

SqlStringLocalizer实现IStringLocalizer。 因为它只是GET资源,所以用作应用程序的单例。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Localization;

public class SqlStringLocalizer : IStringLocalizer
{
private readonly Dictionary<string, string> _localizations;

private readonly string _resourceKey;
public SqlStringLocalizer(Dictionary<string, string> localizations, string resourceKey)
{
_localizations = localizations;
_resourceKey = resourceKey;
}
public LocalizedString this[string name]
{
get
{
return new LocalizedString(name, GetText(name));
}
}

public LocalizedString this[string name, params object[] arguments]
{
get
{
return new LocalizedString(name, GetText(name));
}
}

public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
throw new NotImplementedException();
}

public IStringLocalizer WithCulture(CultureInfo culture)
{
throw new NotImplementedException();
}

private string GetText(string key)
{

#if NET451
var culture = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
#elif NET46
var culture = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
#else
var culture = CultureInfo.CurrentCulture.ToString();
#endif
string computedKey = $"{key}.{culture}";

string result;
if (_localizations.TryGetValue(computedKey, out result))
{
return result;
}
else
{
return _resourceKey + "." + computedKey;
}
}
}
}

LocalizationModelContext类是实体框架核心DbContext实现。 这使用LocalizationRecord模型类进行数据访问。 影子属性用于数据库UpdatedTimestamp,在使用本地化数据库导入进行更新时将使用该属性。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Linq;

using Microsoft.EntityFrameworkCore;

// >dotnet ef migrations add LocalizationMigration
public class LocalizationModelContext : DbContext
{
public LocalizationModelContext(DbContextOptions<LocalizationModelContext> options) :base(options)
{ }

public DbSet<LocalizationRecord> LocalizationRecords { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<LocalizationRecord>().HasKey(m => m.Id);
//builder.Entity<LocalizationRecord>().HasKey(m => m.LocalizationCulture + m.Key);

// shadow properties
builder.Entity<LocalizationRecord>().Property<DateTime>("UpdatedTimestamp");

base.OnModelCreating(builder);
}

public override int SaveChanges()
{
ChangeTracker.DetectChanges();
updateUpdatedProperty<LocalizationRecord>();
return base.SaveChanges();
}

private void updateUpdatedProperty<T>() where T : class
{
var modifiedSourceInfo =
ChangeTracker.Entries<T>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

foreach (var entry in modifiedSourceInfo)
{
entry.Property("UpdatedTimestamp").CurrentValue = DateTime.UtcNow;
}
}
}
}

下一步

该应用程序可以打包为NuGet并添加到NuGet服务器。 该数据库可以优化。 我还可以添加一些额外的配置选项。 我想为SPA json文件以及可以由外部公司翻译的csv文件实现导入,导出功能。

我很乐意提供反馈,非常感谢您提供改进方法的提示。 也许可以将其添加到ASP.NET Core中。