EntityFrameworkCore中的OnModelCreating通过Fluent API配置实体
在我们使用EntityFrameworkCore作为数据库ORM框架的时候,不可避免的要重载DbContext中的一个虚方法OnModelCreating,那么这个方法到底是做什么的?到底有哪些作用呢?带着这些问题我们来看看在EntityFrameworkCore到底该如何使用OnModelCreating这个方法,首先我们来看看Microsoft.EntityFrameworkCore命名空间下面的DbContext中关于OnModelCreating的定义及注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/// <summary> /// Override this method to further configure the model that was discovered by convention from the entity types /// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached /// and re-used for subsequent instances of your derived context. /// </summary> /// <remarks> /// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />) /// then this method will not be run. /// </remarks> /// <param name="modelBuilder"> /// The builder being used to construct the model for this context. Databases (and other extensions) typically /// define extension methods on this object that allow you to configure aspects of the model that are specific /// to a given database. /// </param> protected internal virtual void OnModelCreating(ModelBuilder modelBuilder) { } |
按照官方的解释:在完成对派生上下文的模型的初始化后,并在该模型已锁定并用于初始化上下文之前,将调用此方法。 虽然此方法的默认实现不执行任何操作,但可在派生类中重写此方法,这样便能在锁定模型之前对其进行进一步的配置。通常,在创建派生上下文的第一个实例时仅调用此方法一次, 然后将缓存该上下文的模型,并且该模型适用于应用程序域中的上下文的所有后续实例另外在做数据库迁移生成迁移文件的时候也会调用OnModelCreating方法。通过在给定的 ModelBuidler 上设置 ModelCaching 属性可禁用此缓存,但注意这样做会大大降低性能。 通过直接使用 DbModelBuilder 和 DbContextFactory 类来提供对缓存的更多控制。那么这些深度的自定义配置有哪些方面的内容呢?
一 通过Fluent API配置实体
这个部分是整个配置的重点,我们能够通过Fluent API来进行各种各样的配置,下面通过一系列的例子来加以说明,
1、设置主外键关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#region Sales.Basis 主键及关系设定 modelBuilder.Entity<DealerMarketDptRelation>() .HasOne(d => d.MarketingDepartment) .WithMany(e => e.DealerMarketDptRelations) .HasForeignKey(d => d.MarketId); modelBuilder.Entity<Company>() .HasOne(c => c.Branch) .WithOne(d => d.Company) .HasForeignKey<Branch>(d => d.Id); modelBuilder.Entity<Company>() .HasOne(c => c.Dealer) .WithOne(d => d.Company) .HasForeignKey<Dealer>(d => d.Id); #endregion |
2、设置索引
1
2
3
4
5
6
7
8
|
modelBuilder.Entity<RepairContract>(r => { r.HasIndex(p => p.Code); r.HasIndex(p => p.Vin); r.HasIndex(p => p.DealerCode); r.HasIndex(p => p.DealerName); r.HasIndex(p => p.CreateTime); r.HasIndex(p => p.ModifyTime); }); |
在查询数据库的时候我们经常需添加索引,在Fluent API中我们可以通过上面的方式来为某一个表添加索引。
3、 创建Sequence
1
2
3
4
5
6
7
8
9
10
|
modelBuilder.HasSequence( "S_PdiCheck" ); modelBuilder.HasSequence( "S_PdiCheckItem" ); modelBuilder.HasSequence( "S_PdiCheckItemDetail" ); modelBuilder.HasSequence( "S_PdiCheckAth" ); modelBuilder.HasSequence( "S_CancellationHandlingTime" ); modelBuilder.HasSequence( "S_DealerBlacklist" ); modelBuilder.HasSequence( "S_PropagateMethod" ); modelBuilder.HasSequence( "S_VehicleOwnerChange" ); modelBuilder.HasSequence( "S_CustomerVehicleAth" ); modelBuilder.HasSequence( "S_DealerVehicleStockSnapshots" ); |
4、 设置表级联删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/// <summary> /// 调整生成器的默认行为 /// <para/>生成的表名称为单数,外键关闭级联删除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction( this ModelBuilder modelBuilder) { foreach ( var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null ) continue ; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null ) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach ( var foreignKey in mutableEntityType.GetForeignKeys()) { <br> //设置表级联删除方式 foreignKey.DeleteBehavior = DeleteBehavior.Restrict;<br> } } } |
但是有的时候我们由于业务的需要又需要开启级联删除怎么办,当然我们可以通过OnDelete方法进行删除,就像下面的例子
1
2
3
4
5
|
modelBuilder.Entity<RepairSettlement>(b => { b.HasMany(rs => rs.RepairSettlementWorkItems) .WithOne(rsi => rsi.RepairSettlement) .OnDelete(DeleteBehavior.Cascade); }); |
这里表示的是一组具有主清单关系的例子,其中一个RepairSettlement实体里面可以包含多个RepairSettlementWorkItem实体,这个我们可以在RepairSettlement中定义一个List<RepairSettlementWorkItem>来表示两者之间的关系,然后通过OnDelete方法就会实现两者之间的级联删除关系。
但是如果一个系统中存在大量这种主清单关系需要进行级联删除的话,那么我们的OnModelCreating中就会存在大量这种重复代码,那么有不有一种更加合适的方法用于指定两者之间的级联删除关系呢?
答案是利用自定义属性,直接在实体的外键上面定义好级联删除关系,这样就能最大程度上简化代码。
1 定义ForeignKeyReferenceAttribute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/// <summary> /// 当前自定义属性用于当我们需要开启实体之间的级联删除行为时候使用 /// </summary> public class ForeignKeyReferenceAttribute : Attribute { /// <summary> /// 设置默认的级联删除方式 /// </summary> public ForeignKeyReferenceAttribute() { DeleteBehavior = DeleteBehavior.Restrict; } /// <summary> /// 设置当前外键的级联删除方式 /// </summary> public DeleteBehavior DeleteBehavior { get ; set ; } } |
2 修改设置级联删除的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/// <summary> /// 调整生成器的默认行为 /// <para/>生成的表名称为单数,外键关闭级联删除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction( this ModelBuilder modelBuilder) { foreach ( var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null ) continue ; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null ) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach ( var foreignKey in mutableEntityType.GetForeignKeys()) { var canCascadeDelete = foreignKey.Properties[0]?.PropertyInfo?.GetCustomAttributes<ForeignKeyReferenceAttribute>()?.SingleOrDefault(); foreignKey.DeleteBehavior = canCascadeDelete?.DeleteBehavior ?? DeleteBehavior.Restrict; } } } |
这里不再统一设置foreignKey.DeleteBehavior = DeleteBehavior.Restrict;而是根据当前外键定义的ForeignKeyReferenceAttribute值来决定当前外键的级联删除关系。
3 设置外键级联删除关系
通过上面的例子我们就可以在表RepairSettlementWorkItem中的外键RepairSettlementId上面设置外键级联删除关系。
1
2
|
[ForeignKeyReference(DeleteBehavior = DeleteBehavior.Cascade)] public Guid RepairSettlementId { get ; set ; } |
5、 设置不同数据库专有特性
由于数据库有多种类型,所以不可避免我们需要针对特定数据库来做一些配置,比如Oracle数据库会限定列名最大为30个字符,有比如有些实体中定义的属性,比如Level可能在Oracle数据库中作为一种关键字,而在另外的数据库系统中则没有这个限制,所以我们通过modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName("FLevel"))这种方式来将实体属性名称设置为FLevel,针对这个方面我们也可以在OnModelCreating中统一进行处理。
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
|
if (Database.IsOracle()) { // Oracle 要求列名不超过30个字符 modelBuilder.Entity<NotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); modelBuilder.Entity<NotificationSubscriptionInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); modelBuilder.Entity<TenantNotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); // 日志的时间需要精确 modelBuilder.Entity<AuditLog>(entity => { entity.Property(e => e.ExecutionTime).HasColumnType( "TimeStamp" ); }); modelBuilder.Entity<EntityChange>(entity => { entity.Property(e => e.ChangeTime).HasColumnType( "TimeStamp" ); }); modelBuilder.Entity<EntityChangeSet>(entity => { entity.Property(e => e.CreationTime).HasColumnType( "TimeStamp" ); entity.Property(e => e.ExtensionData).HasMaxLength( int .MaxValue); }); foreach ( var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.ClrType == null ) continue ; if (!entity.ClrType.GetAttributes<TableAttribute>().Any()) { entity.Relational().TableName = entity.ClrType.Name; } foreach ( var property in entity.GetProperties().Where(p => p.PropertyInfo != null )){ if (property.PropertyInfo.Name.ToUpper().EndsWith( "ID" ) && property.ClrType == typeof ( string )) { property.Relational().ColumnType = "CHAR(36)" ; } else if (property.Name == "RowVersion" && (property.ClrType == typeof (DateTime) || property.ClrType == typeof (DateTime?))) { property.Oracle().ColumnType = "TIMESTAMP" ; } else if (property.ClrType == typeof ( decimal ) || property.ClrType == typeof ( decimal ?)) { property.Oracle().ColumnType = "NUMBER(16,4)" ; } } } modelBuilder.Entity<ClaimApplyAth>(d => d.Property(p => p.FileId).HasColumnType( "VARCHAR2(200)" )); modelBuilder.Entity<MarketQualityAth>(d => d.Property(p => p.FileId).HasColumnType( "VARCHAR2(200)" )); modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName( "FLevel" )); } |
再比如表中常用的RowVersion字段在Oracle中实体可定义为DateTime类型,而在SQL Server中就只能够定义为byte[ ]这种方式了,所以在使用的时候都可以通过上面的方式来统一进行处理。
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
|
/// <summary> /// dotConnect的默认DateTime类型是 TimeStamp,无长度限制的String 是 Clob /// </summary> /// <param name="modelBuilder"></param> public static void AdjustOracleDefaultAction( this ModelBuilder modelBuilder) { foreach ( var item in modelBuilder.Model .GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof ( string ) || NoneAnnotationDateTime(p)) .Select(p => new { p.ClrType, p.Name, Pb = modelBuilder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name), MaxLength = p.GetMaxLength() })) { if (item.ClrType == typeof (DateTime?)) { item.Pb.HasColumnType( "date" ); } else if (item.Name == "Discriminator" ) item.Pb.HasMaxLength(100); // ReSharper disable once PossibleInvalidOperationException else if (item.ClrType == typeof ( string ) && item.MaxLength == null ) item.Pb.HasMaxLength(2000); } } |
这里通过一些实际项目中经验来讲述EntityFrameworkCore的一些特性,后续有进一步的新的内容也会不断加入,从而使文章内容更加丰富。
一 通过Fluent API配置实体
这个部分是整个配置的重点,我们能够通过Fluent API来进行各种各样的配置,下面通过一系列的例子来加以说明,这里推荐一篇非常好的文章,这里便不再赘述,这里只讲述之前不同的部分,从而使整篇文章更加完整,如果你对数据库中的关系映射不太清楚,请阅读这篇文章。
A 设置主外键关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#region Sales.Basis 主键及关系设定 modelBuilder.Entity<DealerMarketDptRelation>() .HasOne(d => d.MarketingDepartment) .WithMany(e => e.DealerMarketDptRelations) .HasForeignKey(d => d.MarketId); modelBuilder.Entity<Company>() .HasOne(c => c.Branch) .WithOne(d => d.Company) .HasForeignKey<Branch>(d => d.Id); modelBuilder.Entity<Company>() .HasOne(c => c.Dealer) .WithOne(d => d.Company) .HasForeignKey<Dealer>(d => d.Id); #endregion |
B 设置索引
1
2
3
4
5
6
7
8
|
modelBuilder.Entity<RepairContract>(r => { r.HasIndex(p => p.Code); r.HasIndex(p => p.Vin); r.HasIndex(p => p.DealerCode); r.HasIndex(p => p.DealerName); r.HasIndex(p => p.CreateTime); r.HasIndex(p => p.ModifyTime); }); |
在查询数据库的时候我们经常需添加索引,在Fluent API中我们可以通过上面的方式来为某一个表添加索引。
C 创建Sequence
1
2
3
4
5
6
7
8
9
10
|
modelBuilder.HasSequence( "S_PdiCheck" ); modelBuilder.HasSequence( "S_PdiCheckItem" ); modelBuilder.HasSequence( "S_PdiCheckItemDetail" ); modelBuilder.HasSequence( "S_PdiCheckAth" ); modelBuilder.HasSequence( "S_CancellationHandlingTime" ); modelBuilder.HasSequence( "S_DealerBlacklist" ); modelBuilder.HasSequence( "S_PropagateMethod" ); modelBuilder.HasSequence( "S_VehicleOwnerChange" ); modelBuilder.HasSequence( "S_CustomerVehicleAth" ); modelBuilder.HasSequence( "S_DealerVehicleStockSnapshots" ); |
D 设置表级联删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/// <summary> /// 调整生成器的默认行为 /// <para/>生成的表名称为单数,外键关闭级联删除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction( this ModelBuilder modelBuilder) { foreach ( var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null ) continue ; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null ) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach ( var foreignKey in mutableEntityType.GetForeignKeys()) { <br> //设置表级联删除方式 foreignKey.DeleteBehavior = DeleteBehavior.Restrict;<br> } } } |
但是有的时候我们由于业务的需要又需要开启级联删除怎么办,当然我们可以通过OnDelete方法进行删除,就像下面的例子
1
2
3
4
5
|
modelBuilder.Entity<RepairSettlement>(b => { b.HasMany(rs => rs.RepairSettlementWorkItems) .WithOne(rsi => rsi.RepairSettlement) .OnDelete(DeleteBehavior.Cascade); }); |
这里表示的是一组具有主清单关系的例子,其中一个RepairSettlement实体里面可以包含多个RepairSettlementWorkItem实体,这个我们可以在RepairSettlement中定义一个List<RepairSettlementWorkItem>来表示两者之间的关系,然后通过OnDelete方法就会实现两者之间的级联删除关系。
但是如果一个系统中存在大量这种主清单关系需要进行级联删除的话,那么我们的OnModelCreating中就会存在大量这种重复代码,那么有不有一种更加合适的方法用于指定两者之间的级联删除关系呢?
答案是利用自定义属性,直接在实体的外键上面定义好级联删除关系,这样就能最大程度上简化代码。
1 定义ForeignKeyReferenceAttribute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/// <summary> /// 当前自定义属性用于当我们需要开启实体之间的级联删除行为时候使用 /// </summary> public class ForeignKeyReferenceAttribute : Attribute { /// <summary> /// 设置默认的级联删除方式 /// </summary> public ForeignKeyReferenceAttribute() { DeleteBehavior = DeleteBehavior.Restrict; } /// <summary> /// 设置当前外键的级联删除方式 /// </summary> public DeleteBehavior DeleteBehavior { get ; set ; } } |
2 修改设置级联删除的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/// <summary> /// 调整生成器的默认行为 /// <para/>生成的表名称为单数,外键关闭级联删除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction( this ModelBuilder modelBuilder) { foreach ( var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null ) continue ; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null ) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach ( var foreignKey in mutableEntityType.GetForeignKeys()) { var canCascadeDelete = foreignKey.Properties[0]?.PropertyInfo?.GetCustomAttributes<ForeignKeyReferenceAttribute>()?.SingleOrDefault(); foreignKey.DeleteBehavior = canCascadeDelete?.DeleteBehavior ?? DeleteBehavior.Restrict; } } } |
这里不再统一设置foreignKey.DeleteBehavior = DeleteBehavior.Restrict;而是根据当前外键定义的ForeignKeyReferenceAttribute值来决定当前外键的级联删除关系。
3 设置外键级联删除关系
通过上面的例子我们就可以在表RepairSettlementWorkItem中的外键RepairSettlementId上面设置外键级联删除关系。
1
2
|
[ForeignKeyReference(DeleteBehavior = DeleteBehavior.Cascade)] public Guid RepairSettlementId { get ; set ; } |
E 设置不同数据库专有特性
由于数据库有多种类型,所以不可避免我们需要针对特定数据库来做一些配置,比如Oracle数据库会限定列名最大为30个字符,有比如有些实体中定义的属性,比如Level可能在Oracle数据库中作为一种关键字,而在另外的数据库系统中则没有这个限制,所以我们通过modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName("FLevel"))这种方式来将实体属性名称设置为FLevel,针对这个方面我们也可以在OnModelCreating中统一进行处理。
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
|
if (Database.IsOracle()) { // Oracle 要求列名不超过30个字符 modelBuilder.Entity<NotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); modelBuilder.Entity<NotificationSubscriptionInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); modelBuilder.Entity<TenantNotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName( "EntityTypeAssyQualifiedName" ); }); // 日志的时间需要精确 modelBuilder.Entity<AuditLog>(entity => { entity.Property(e => e.ExecutionTime).HasColumnType( "TimeStamp" ); }); modelBuilder.Entity<EntityChange>(entity => { entity.Property(e => e.ChangeTime).HasColumnType( "TimeStamp" ); }); modelBuilder.Entity<EntityChangeSet>(entity => { entity.Property(e => e.CreationTime).HasColumnType( "TimeStamp" ); entity.Property(e => e.ExtensionData).HasMaxLength( int .MaxValue); }); foreach ( var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.ClrType == null ) continue ; if (!entity.ClrType.GetAttributes<TableAttribute>().Any()) { entity.Relational().TableName = entity.ClrType.Name; } foreach ( var property in entity.GetProperties().Where(p => p.PropertyInfo != null )){ if (property.PropertyInfo.Name.ToUpper().EndsWith( "ID" ) && property.ClrType == typeof ( string )) { property.Relational().ColumnType = "CHAR(36)" ; } else if (property.Name == "RowVersion" && (property.ClrType == typeof (DateTime) || property.ClrType == typeof (DateTime?))) { property.Oracle().ColumnType = "TIMESTAMP" ; } else if (property.ClrType == typeof ( decimal ) || property.ClrType == typeof ( decimal ?)) { property.Oracle().ColumnType = "NUMBER(16,4)" ; } } } modelBuilder.Entity<ClaimApplyAth>(d => d.Property(p => p.FileId).HasColumnType( "VARCHAR2(200)" )); modelBuilder.Entity<MarketQualityAth>(d => d.Property(p => p.FileId).HasColumnType( "VARCHAR2(200)" )); modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName( "FLevel" )); } |
再比如表中常用的RowVersion字段在Oracle中实体可定义为DateTime类型,而在SQL Server中就只能够定义为byte[ ]这种方式了,所以在使用的时候都可以通过上面的方式来统一进行处理。
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
|
/// <summary> /// dotConnect的默认DateTime类型是 TimeStamp,无长度限制的String 是 Clob /// </summary> /// <param name="modelBuilder"></param> public static void AdjustOracleDefaultAction( this ModelBuilder modelBuilder) { foreach ( var item in modelBuilder.Model .GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof ( string ) || NoneAnnotationDateTime(p)) .Select(p => new { p.ClrType, p.Name, Pb = modelBuilder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name), MaxLength = p.GetMaxLength() })) { if (item.ClrType == typeof (DateTime?)) { item.Pb.HasColumnType( "date" ); } else if (item.Name == "Discriminator" ) item.Pb.HasMaxLength(100); // ReSharper disable once PossibleInvalidOperationException else if (item.ClrType == typeof ( string ) && item.MaxLength == null ) item.Pb.HasMaxLength(2000); } } |
这里通过一些实际项目中经验来讲述EntityFrameworkCore的一些特性,后续有进一步的新的内容也会不断加入,从而使文章内容更加丰富。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)