diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5f8e54da9d..3c72840715 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -149,6 +149,41 @@ jobs:
path: |
**/TestResults/*
**/logs/*
+ test-efcore-sqlserver:
+ name: Microsoft Entity Framework Core SQL Server provider tests
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ provider: [ "EFCore-SqlServer" ]
+ services:
+ mssql:
+ image: mcr.microsoft.com/mssql/server:latest
+ ports:
+ - 1433:1433
+ env:
+ ACCEPT_EULA: "Y"
+ MSSQL_PID: "Developer"
+ # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="False positive")]
+ SA_PASSWORD: "yourStrong(!)Password"
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: |
+ 3.1.x
+ 7.0.x
+ - name: Test
+ run: dotnet test --filter "Category=${{ matrix.provider }}&(Category=BVT|Category=SlowBVT|Category=Functional)" --blame-hang-timeout 10m --logger "trx" -- -parallel none -noshadow
+ - name: Archive Test Results
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: test_output
+ retention-days: 1
+ path: |
+ **/TestResults/*
+ **/logs/*
test-sqlserver:
name: Microsoft SQL Server provider tests
runs-on: ubuntu-latest
diff --git a/Directory.Packages.props b/Directory.Packages.props
index be0fb3f5ab..fdc403e502 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,6 +54,7 @@
+
diff --git a/Orleans.sln b/Orleans.sln
index cbb8321f23..310a776594 100644
--- a/Orleans.sln
+++ b/Orleans.sln
@@ -246,6 +246,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orleans.Persistence.EntityF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orleans.Reminders.EntityFrameworkCore.SqlServer", "src\EFCore\Orleans.Reminders.EntityFrameworkCore.SqlServer\Orleans.Reminders.EntityFrameworkCore.SqlServer.csproj", "{CC8ECC81-4160-47E9-B9D7-E578BAC424F7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orleans.Clustering.EntityFrameworkCore.MySql", "src\EFCore\Orleans.Clustering.EntityFrameworkCore.MySql\Orleans.Clustering.EntityFrameworkCore.MySql.csproj", "{AA369C0C-9941-469E-BD5C-E5E5DB632431}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -652,6 +654,10 @@ Global
{CC8ECC81-4160-47E9-B9D7-E578BAC424F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC8ECC81-4160-47E9-B9D7-E578BAC424F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC8ECC81-4160-47E9-B9D7-E578BAC424F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA369C0C-9941-469E-BD5C-E5E5DB632431}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA369C0C-9941-469E-BD5C-E5E5DB632431}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA369C0C-9941-469E-BD5C-E5E5DB632431}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA369C0C-9941-469E-BD5C-E5E5DB632431}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -771,6 +777,7 @@ Global
{54F742D9-B721-4B2D-98F1-B2EDA8E8C70F} = {616ECAC3-2EA7-4819-9A05-EF4F4D8DDFA8}
{95F5EC32-BEFC-4FBA-8BEA-857007AEB9C6} = {616ECAC3-2EA7-4819-9A05-EF4F4D8DDFA8}
{CC8ECC81-4160-47E9-B9D7-E578BAC424F7} = {616ECAC3-2EA7-4819-9A05-EF4F4D8DDFA8}
+ {AA369C0C-9941-469E-BD5C-E5E5DB632431} = {616ECAC3-2EA7-4819-9A05-EF4F4D8DDFA8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7BFB3429-B5BB-4DB1-95B4-67D77A864952}
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Clustering.sql b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Clustering.sql
new file mode 100644
index 0000000000..744b53c31f
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Clustering.sql
@@ -0,0 +1,63 @@
+CREATE TABLE IF NOT EXISTS `__EFMigrationsHistory` (
+ `MigrationId` varchar(150) NOT NULL,
+ `ProductVersion` varchar(32) NOT NULL,
+ PRIMARY KEY (`MigrationId`)
+);
+
+START TRANSACTION;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ CREATE TABLE `Clusters` (
+ `Id` varchar(255) NOT NULL,
+ `Timestamp` datetime(6) NOT NULL,
+ `Version` int NOT NULL,
+ `ETag` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (`Id`)
+ );
+END;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ CREATE TABLE `Silos` (
+ `ClusterId` varchar(255) NOT NULL,
+ `Address` varchar(45) NOT NULL,
+ `Port` int NOT NULL,
+ `Generation` int NOT NULL,
+ `Name` varchar(150) NOT NULL,
+ `HostName` varchar(150) NOT NULL,
+ `Status` int NOT NULL,
+ `ProxyPort` int NULL,
+ `SuspectingTimes` longtext NULL,
+ `SuspectingSilos` longtext NULL,
+ `StartTime` datetime(6) NOT NULL,
+ `IAmAliveTime` datetime(6) NOT NULL,
+ `ETag` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
+ PRIMARY KEY (`ClusterId`, `Address`, `Port`, `Generation`),
+ CONSTRAINT `FK_Silos_Clusters_ClusterId` FOREIGN KEY (`ClusterId`) REFERENCES `Clusters` (`Id`) ON DELETE CASCADE
+ );
+END;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ CREATE INDEX `IDX_Silo_ClusterId` ON `Silos` (`ClusterId`);
+END;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ CREATE INDEX `IDX_Silo_ClusterId_Status` ON `Silos` (`ClusterId`, `Status`);
+END;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ CREATE INDEX `IDX_Silo_ClusterId_Status_IAmAlive` ON `Silos` (`ClusterId`, `Status`, `IAmAliveTime`);
+END;
+
+IF NOT EXISTS(SELECT * FROM `__EFMigrationsHistory` WHERE `MigrationId` = '20231007024046_InitialClusteringSchema')
+BEGIN
+ INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
+ VALUES ('20231007024046_InitialClusteringSchema', '7.0.11');
+END;
+
+COMMIT;
+
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.Designer.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.Designer.cs
new file mode 100644
index 0000000000..eb65442d71
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.Designer.cs
@@ -0,0 +1,128 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Orleans.Clustering.EntityFrameworkCore.MySql.Data;
+
+#nullable disable
+
+namespace Orleans.Clustering.EntityFrameworkCore.MySql.Data.Migrations
+{
+ [DbContext(typeof(MySqlClusterDbContext))]
+ [Migration("20231007024046_InitialClusteringSchema")]
+ partial class InitialClusteringSchema
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(255)");
+
+ b.Property("ETag")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime(6)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Version")
+ .HasColumnType("int");
+
+ b.HasKey("Id")
+ .HasName("PK_Cluster");
+
+ b.ToTable("Clusters");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.SiloRecord", b =>
+ {
+ b.Property("ClusterId")
+ .HasColumnType("varchar(255)");
+
+ b.Property("Address")
+ .HasMaxLength(45)
+ .HasColumnType("varchar(45)");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("Generation")
+ .HasColumnType("int");
+
+ b.Property("ETag")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime(6)");
+
+ b.Property("HostName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("IAmAliveTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("ProxyPort")
+ .HasColumnType("int");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("SuspectingSilos")
+ .HasColumnType("longtext");
+
+ b.Property("SuspectingTimes")
+ .HasColumnType("longtext");
+
+ b.HasKey("ClusterId", "Address", "Port", "Generation")
+ .HasName("PK_Silo");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IDX_Silo_ClusterId");
+
+ b.HasIndex("ClusterId", "Status")
+ .HasDatabaseName("IDX_Silo_ClusterId_Status");
+
+ b.HasIndex("ClusterId", "Status", "IAmAliveTime")
+ .HasDatabaseName("IDX_Silo_ClusterId_Status_IAmAlive");
+
+ b.ToTable("Silos");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.SiloRecord", b =>
+ {
+ b.HasOne("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", "Cluster")
+ .WithMany("Silos")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", b =>
+ {
+ b.Navigation("Silos");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.cs
new file mode 100644
index 0000000000..bf418fc620
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/20231007024046_InitialClusteringSchema.cs
@@ -0,0 +1,91 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using MySql.EntityFrameworkCore.Metadata;
+
+#nullable disable
+
+namespace Orleans.Clustering.EntityFrameworkCore.MySql.Data.Migrations
+{
+ ///
+ public partial class InitialClusteringSchema : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("MySQL:Charset", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Clusters",
+ columns: table => new
+ {
+ Id = table.Column(type: "varchar(255)", nullable: false),
+ Timestamp = table.Column(type: "datetime(6)", nullable: false),
+ Version = table.Column(type: "int", nullable: false),
+ ETag = table.Column(type: "datetime(6)", rowVersion: true, nullable: false)
+ .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.ComputedColumn)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Cluster", x => x.Id);
+ })
+ .Annotation("MySQL:Charset", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "Silos",
+ columns: table => new
+ {
+ ClusterId = table.Column(type: "varchar(255)", nullable: false),
+ Address = table.Column(type: "varchar(45)", maxLength: 45, nullable: false),
+ Port = table.Column(type: "int", nullable: false),
+ Generation = table.Column(type: "int", nullable: false),
+ Name = table.Column(type: "varchar(150)", maxLength: 150, nullable: false),
+ HostName = table.Column(type: "varchar(150)", maxLength: 150, nullable: false),
+ Status = table.Column(type: "int", nullable: false),
+ ProxyPort = table.Column(type: "int", nullable: true),
+ SuspectingTimes = table.Column(type: "longtext", nullable: true),
+ SuspectingSilos = table.Column(type: "longtext", nullable: true),
+ StartTime = table.Column(type: "datetime(6)", nullable: false),
+ IAmAliveTime = table.Column(type: "datetime(6)", nullable: false),
+ ETag = table.Column(type: "datetime(6)", rowVersion: true, nullable: false)
+ .Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.ComputedColumn)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Silo", x => new { x.ClusterId, x.Address, x.Port, x.Generation });
+ table.ForeignKey(
+ name: "FK_Silos_Clusters_ClusterId",
+ column: x => x.ClusterId,
+ principalTable: "Clusters",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ })
+ .Annotation("MySQL:Charset", "utf8mb4");
+
+ migrationBuilder.CreateIndex(
+ name: "IDX_Silo_ClusterId",
+ table: "Silos",
+ column: "ClusterId");
+
+ migrationBuilder.CreateIndex(
+ name: "IDX_Silo_ClusterId_Status",
+ table: "Silos",
+ columns: new[] { "ClusterId", "Status" });
+
+ migrationBuilder.CreateIndex(
+ name: "IDX_Silo_ClusterId_Status_IAmAlive",
+ table: "Silos",
+ columns: new[] { "ClusterId", "Status", "IAmAliveTime" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Silos");
+
+ migrationBuilder.DropTable(
+ name: "Clusters");
+ }
+ }
+}
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/MySqlClusterDbContextModelSnapshot.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/MySqlClusterDbContextModelSnapshot.cs
new file mode 100644
index 0000000000..b43b333031
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/Migrations/MySqlClusterDbContextModelSnapshot.cs
@@ -0,0 +1,125 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Orleans.Clustering.EntityFrameworkCore.MySql.Data;
+
+#nullable disable
+
+namespace Orleans.Clustering.EntityFrameworkCore.MySql.Data.Migrations
+{
+ [DbContext(typeof(MySqlClusterDbContext))]
+ partial class MySqlClusterDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("varchar(255)");
+
+ b.Property("ETag")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime(6)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Version")
+ .HasColumnType("int");
+
+ b.HasKey("Id")
+ .HasName("PK_Cluster");
+
+ b.ToTable("Clusters");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.SiloRecord", b =>
+ {
+ b.Property("ClusterId")
+ .HasColumnType("varchar(255)");
+
+ b.Property("Address")
+ .HasMaxLength(45)
+ .HasColumnType("varchar(45)");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("Generation")
+ .HasColumnType("int");
+
+ b.Property("ETag")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("datetime(6)");
+
+ b.Property("HostName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("IAmAliveTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("ProxyPort")
+ .HasColumnType("int");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("SuspectingSilos")
+ .HasColumnType("longtext");
+
+ b.Property("SuspectingTimes")
+ .HasColumnType("longtext");
+
+ b.HasKey("ClusterId", "Address", "Port", "Generation")
+ .HasName("PK_Silo");
+
+ b.HasIndex("ClusterId")
+ .HasDatabaseName("IDX_Silo_ClusterId");
+
+ b.HasIndex("ClusterId", "Status")
+ .HasDatabaseName("IDX_Silo_ClusterId_Status");
+
+ b.HasIndex("ClusterId", "Status", "IAmAliveTime")
+ .HasDatabaseName("IDX_Silo_ClusterId_Status_IAmAlive");
+
+ b.ToTable("Silos");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.SiloRecord", b =>
+ {
+ b.HasOne("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", "Cluster")
+ .WithMany("Silos")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("Orleans.Clustering.EntityFrameworkCore.Data.ClusterRecord", b =>
+ {
+ b.Navigation("Silos");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContext.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContext.cs
new file mode 100644
index 0000000000..5d2d541ae4
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContext.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Orleans.Clustering.EntityFrameworkCore.Data;
+
+namespace Orleans.Clustering.EntityFrameworkCore.MySql.Data;
+
+public class MySqlClusterDbContext : ClusterDbContext
+{
+ public MySqlClusterDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity>(c =>
+ {
+ c.HasKey(p => p.Id).HasName("PK_Cluster");
+ c.Property(p => p.Timestamp).IsRequired();
+ c.Property(p => p.Version).IsRequired();
+ c.Property(p => p.ETag).IsRowVersion().IsConcurrencyToken();
+
+ c
+ .HasMany(p => p.Silos)
+ .WithOne(r => r.Cluster)
+ .HasForeignKey(r => r.ClusterId);
+ });
+
+ var listToStringConverter = new ValueConverter, string>(
+ v => string.Join(",", v),
+ v => v.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).ToList());
+
+
+ var listComparer = new ValueComparer>(
+ (c1, c2) => (c1 == null && c2 == null) || (c1 != null && c2 != null && c1.SequenceEqual(c2)),
+ c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
+ c => new List(c));
+
+ modelBuilder.Entity>(c =>
+ {
+ c.HasKey(p => new {p.ClusterId, p.Address, p.Port, p.Generation}).HasName("PK_Silo");
+ c.Property(p => p.Address).HasMaxLength(45).IsRequired();
+ c.Property(p => p.Port).IsRequired();
+ c.Property(p => p.Generation).IsRequired();
+ c.Property(p => p.Name).HasMaxLength(150).IsRequired();
+ c.Property(p => p.HostName).HasMaxLength(150).IsRequired();
+ c.Property(p => p.Status).IsRequired();
+ c.Property(p => p.ProxyPort).IsRequired(false);
+ c.Property(p => p.SuspectingTimes).IsRequired(false).HasConversion(listToStringConverter).Metadata.SetValueComparer(listComparer);
+ c.Property(p => p.SuspectingSilos).IsRequired(false).HasConversion(listToStringConverter).Metadata.SetValueComparer(listComparer);
+ c.Property(p => p.StartTime).IsRequired();
+ c.Property(p => p.IAmAliveTime).IsRequired();
+ c.Property(p => p.ETag).IsRowVersion().IsConcurrencyToken();
+
+ c
+ .HasOne(p => p.Cluster)
+ .WithMany(p => p.Silos)
+ .HasForeignKey(p => p.ClusterId);
+
+ c.HasIndex(p => p.ClusterId).HasDatabaseName("IDX_Silo_ClusterId");
+ c.HasIndex(p => new {p.ClusterId, p.Status}).HasDatabaseName("IDX_Silo_ClusterId_Status");
+ c.HasIndex(p => new {p.ClusterId, p.Status, p.IAmAliveTime}).HasDatabaseName("IDX_Silo_ClusterId_Status_IAmAlive");
+ });
+ }
+
+ // public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
+ // {
+ // foreach (var entity in ChangeTracker.Entries().Where(e => e.State is EntityState.Modified or EntityState.Added))
+ // {
+ // switch (entity.Entity)
+ // {
+ // case ClusterRecord clusterRecord:
+ // clusterRecord.ETag = Guid.NewGuid();
+ // continue;
+ // case SiloRecord siloRecord:
+ // siloRecord.ETag = Guid.NewGuid();
+ // continue;
+ // }
+ // }
+ //
+ // return base.SaveChangesAsync(cancellationToken);
+ // }
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContextFactory.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContextFactory.cs
new file mode 100644
index 0000000000..3cb7406764
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Data/MySqlClusterDbContextFactory.cs
@@ -0,0 +1,18 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace Orleans.Clustering.EntityFrameworkCore.MySql.Data;
+
+public class MySqlClusterDbContextFactory: IDesignTimeDbContextFactory
+{
+ public MySqlClusterDbContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseMySQL("Data Source=db.db", opt =>
+ {
+ opt.MigrationsHistoryTable("__EFMigrationsHistory");
+ opt.MigrationsAssembly(typeof(MySqlClusterDbContext).Assembly.FullName);
+ });
+ return new MySqlClusterDbContext(optionsBuilder.Options);
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlClusterETagConverter.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlClusterETagConverter.cs
new file mode 100644
index 0000000000..1a15c0caac
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlClusterETagConverter.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Globalization;
+using Orleans.Clustering.EntityFrameworkCore;
+
+namespace Orleans.Clustering;
+
+public class MySqlClusterETagConverter : IEFClusterETagConverter
+{
+ public DateTime ToDbETag(string etag) => DateTime.Parse(etag, CultureInfo.InvariantCulture);
+
+ public string FromDbETag(DateTime etag) => etag.ToString(CultureInfo.InvariantCulture);
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlHostingExtensions.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlHostingExtensions.cs
new file mode 100644
index 0000000000..3b9f84d84f
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/MySqlHostingExtensions.cs
@@ -0,0 +1,101 @@
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Orleans.Hosting;
+using Orleans.Clustering.EntityFrameworkCore;
+using Orleans.Clustering.EntityFrameworkCore.MySql.Data;
+
+namespace Orleans.Clustering;
+
+public static class MySqlHostingExtensions
+{
+ ///
+ /// Configures the silo to use Entity Framework Core for clustering with MySQL.
+ ///
+ ///
+ /// The silo builder.
+ ///
+ ///
+ /// The database configuration delegate.
+ ///
+ ///
+ /// The provided .
+ ///
+ public static ISiloBuilder UseEntityFrameworkCoreMySqlClustering(
+ this ISiloBuilder builder,
+ Action configureDatabase)
+ {
+ return builder
+ .ConfigureServices(services =>
+ {
+ services.AddPooledDbContextFactory(configureDatabase);
+ })
+ .UseEntityFrameworkCoreMySqlClustering();
+ }
+
+ ///
+ /// Configures the silo to use Entity Framework Core for clustering with MySQL.
+ /// This overload expects a to be registered already.
+ ///
+ ///
+ /// The silo builder.
+ ///
+ ///
+ /// The provided .
+ ///
+ public static ISiloBuilder UseEntityFrameworkCoreMySqlClustering(this ISiloBuilder builder)
+ {
+ return builder
+ .ConfigureServices(services =>
+ {
+ services
+ .AddSingleton, MySqlClusterETagConverter>();
+ })
+ .UseEntityFrameworkCoreClustering();
+ }
+
+ ///
+ /// Configures the silo to use Entity Framework Core for clustering with MySQL.
+ ///
+ ///
+ /// The silo builder.
+ ///
+ ///
+ /// The database configuration delegate.
+ ///
+ ///
+ /// The provided .
+ ///
+ public static IClientBuilder UseEntityFrameworkCoreMySqlClustering(
+ this IClientBuilder builder,
+ Action configureDatabase)
+ {
+ return builder
+ .ConfigureServices(services =>
+ {
+ services.AddPooledDbContextFactory(configureDatabase);
+ })
+ .UseEntityFrameworkCoreMySqlClustering();
+ }
+
+ ///
+ /// Configures the silo to use Entity Framework Core for clustering with MySQL.
+ /// This overload expects a to be registered already.
+ ///
+ ///
+ /// The silo builder.
+ ///
+ ///
+ /// The provided .
+ ///
+ public static IClientBuilder UseEntityFrameworkCoreMySqlClustering(
+ this IClientBuilder builder)
+ {
+ return builder
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton, MySqlClusterETagConverter>();
+ })
+ .UseEntityFrameworkCoreClustering();
+ }
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Orleans.Clustering.EntityFrameworkCore.MySql.csproj b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Orleans.Clustering.EntityFrameworkCore.MySql.csproj
new file mode 100644
index 0000000000..f6c23e44cb
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.MySql/Orleans.Clustering.EntityFrameworkCore.MySql.csproj
@@ -0,0 +1,29 @@
+
+
+
+ Microsoft.Orleans.Clustering.EntityFrameworkCore.MySql
+ Microsoft Orleans Entity Framework Core (MySql) Clustering Provider
+ Microsoft Orleans clustering provider backed by Entity Framework Core for MySQL
+ $(PackageTags) Entity Framework Core SQL MySQL PostgreSQL Oracle
+ enable
+ $(DefaultTargetFrameworks)
+
+
+
+ Orleans.Clustering.EntityFrameworkCore.MySql
+ Orleans.Clustering.EntityFrameworkCore.MySql
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
+
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Data/SqlServerClusterDbContext.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Data/SqlServerClusterDbContext.cs
index 4624133157..e89488ac4e 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Data/SqlServerClusterDbContext.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Data/SqlServerClusterDbContext.cs
@@ -1,6 +1,6 @@
using System;
-using System.Collections.Generic;
using System.Linq;
+using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -8,7 +8,7 @@
namespace Orleans.Clustering.EntityFrameworkCore.SqlServer.Data;
-public sealed class SqlServerClusterDbContext : ClusterDbContext
+public sealed class SqlServerClusterDbContext : ClusterDbContext
{
public SqlServerClusterDbContext(DbContextOptions options) : base(options)
{
@@ -16,7 +16,7 @@ public SqlServerClusterDbContext(DbContextOptions opt
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
- modelBuilder.Entity(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => p.Id).IsClustered(false).HasName("PK_Cluster");
c.Property(p => p.Timestamp).IsRequired();
@@ -39,7 +39,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => new List(c));
- modelBuilder.Entity(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => new {p.ClusterId, p.Address, p.Port, p.Generation}).IsClustered(false).HasName("PK_Silo");
c.Property(p => p.Address).HasMaxLength(45).IsRequired();
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Orleans.Clustering.EntityFrameworkCore.SqlServer.csproj b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Orleans.Clustering.EntityFrameworkCore.SqlServer.csproj
index ff2e5afd92..d71a4b121c 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Orleans.Clustering.EntityFrameworkCore.SqlServer.csproj
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/Orleans.Clustering.EntityFrameworkCore.SqlServer.csproj
@@ -14,6 +14,10 @@
Orleans.Clustering.EntityFrameworkCore.SqlServer
+
+
+
+
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerClusterETagConverter.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerClusterETagConverter.cs
new file mode 100644
index 0000000000..599b4c7452
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerClusterETagConverter.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Orleans.Clustering.EntityFrameworkCore.SqlServer;
+
+internal class SqlServerClusterETagConverter : IEFClusterETagConverter
+{
+ public byte[] ToDbETag(string etag) => BitConverter.GetBytes(ulong.Parse(etag));
+
+ public string FromDbETag(byte[] etag) => BitConverter.ToUInt64(etag).ToString();
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
index ee898a5b3c..f8e6105007 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
@@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using Orleans.Hosting;
using Orleans.Clustering.EntityFrameworkCore;
+using Orleans.Clustering.EntityFrameworkCore.SqlServer;
using Orleans.Clustering.EntityFrameworkCore.SqlServer.Data;
namespace Orleans.Clustering;
@@ -30,7 +31,7 @@ public static ISiloBuilder UseEntityFrameworkCoreSqlServerClustering(
{
services.AddPooledDbContextFactory(configureDatabase);
})
- .UseEntityFrameworkCoreClustering();
+ .UseEntityFrameworkCoreSqlServerClustering();
}
///
@@ -48,8 +49,10 @@ public static ISiloBuilder UseEntityFrameworkCoreSqlServerClustering(this ISiloB
return builder
.ConfigureServices(services =>
{
- services.AddSingleton>();
- });
+ services
+ .AddSingleton, SqlServerClusterETagConverter>();
+ })
+ .UseEntityFrameworkCoreClustering();
}
///
@@ -73,7 +76,7 @@ public static IClientBuilder UseEntityFrameworkCoreSqlServerClustering(
{
services.AddPooledDbContextFactory(configureDatabase);
})
- .UseEntityFrameworkCoreClustering();
+ .UseEntityFrameworkCoreSqlServerClustering();
}
///
@@ -89,6 +92,11 @@ public static IClientBuilder UseEntityFrameworkCoreSqlServerClustering(
public static IClientBuilder UseEntityFrameworkCoreSqlServerClustering(
this IClientBuilder builder)
{
- return builder.UseEntityFrameworkCoreClustering();
+ return builder
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton, SqlServerClusterETagConverter>();
+ })
+ .UseEntityFrameworkCoreClustering();
}
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterDbContext.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterDbContext.cs
index 69793a9ba2..41d0367e4f 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterDbContext.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterDbContext.cs
@@ -2,10 +2,10 @@
namespace Orleans.Clustering.EntityFrameworkCore.Data;
-public class ClusterDbContext : DbContext where TDbContext : DbContext
+public class ClusterDbContext : DbContext where TDbContext : DbContext
{
- public DbSet Clusters { get; set; } = default!;
- public DbSet Silos { get; set; } = default!;
+ public DbSet> Clusters { get; set; } = default!;
+ public DbSet> Silos { get; set; } = default!;
public ClusterDbContext(DbContextOptions options) : base(options)
{
@@ -13,7 +13,7 @@ public ClusterDbContext(DbContextOptions options) : base(options)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
- modelBuilder.Entity(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => p.Id);
c.Property(p => p.Timestamp).IsRequired();
@@ -26,7 +26,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasForeignKey(r => r.ClusterId);
});
- modelBuilder.Entity(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => new {p.ClusterId, p.Address, p.Port, p.Generation});
c.Property(p => p.Address).HasMaxLength(45).IsRequired();
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterRecord.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterRecord.cs
index 468ff0fa32..a528e1f799 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterRecord.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/ClusterRecord.cs
@@ -3,11 +3,11 @@
namespace Orleans.Clustering.EntityFrameworkCore.Data;
-public class ClusterRecord
+public class ClusterRecord
{
public string Id { get; set; } = default!;
public DateTimeOffset Timestamp { get; set; }
public int Version { get; set; }
- public byte[] ETag { get; set; } = Array.Empty();
- public List Silos { get; set; } = new();
+ public TETag ETag { get; set; } = default!;
+ public List> Silos { get; set; } = new();
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/SiloRecord.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/SiloRecord.cs
index 48eacd4601..691ca032ef 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/SiloRecord.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Data/SiloRecord.cs
@@ -4,7 +4,7 @@
namespace Orleans.Clustering.EntityFrameworkCore.Data;
-public class SiloRecord
+public class SiloRecord
{
public string ClusterId { get; set; } = default!;
public string Address { get; set; } = default!;
@@ -18,6 +18,6 @@ public class SiloRecord
public List SuspectingSilos { get; set; } = new();
public DateTimeOffset StartTime { get; set; }
public DateTimeOffset IAmAliveTime { get; set; }
- public byte[] ETag { get; set; } = Array.Empty();
- public ClusterRecord Cluster { get; set; } = default!;
+ public TETag ETag { get; set; } = default!;
+ public ClusterRecord Cluster { get; set; } = default!;
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFClusteringExtensions.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFClusteringExtensions.cs
index 57aac11a57..4d1b2e57de 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFClusteringExtensions.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFClusteringExtensions.cs
@@ -22,21 +22,21 @@ public static class EFClusteringExtensions
///
/// The provided .
///
- public static ISiloBuilder UseEntityFrameworkCoreClustering(
+ public static ISiloBuilder UseEntityFrameworkCoreClustering(
this ISiloBuilder builder,
- Action configureDatabase) where TDbContext : ClusterDbContext
+ Action configureDatabase) where TDbContext : ClusterDbContext
{
return builder
.ConfigureServices(services =>
{
services.AddPooledDbContextFactory(configureDatabase);
})
- .UseEntityFrameworkCoreClustering();
+ .UseEntityFrameworkCoreClustering();
}
///
/// Configures the silo to use Entity Framework Core for clustering.
- /// This overload expects a to be registered already.
+ /// This overload expects a to be registered already.
///
///
/// The silo builder.
@@ -44,13 +44,14 @@ public static ISiloBuilder UseEntityFrameworkCoreClustering(
///
/// The provided .
///
- public static ISiloBuilder UseEntityFrameworkCoreClustering(
- this ISiloBuilder builder) where TDbContext : ClusterDbContext
+ public static ISiloBuilder UseEntityFrameworkCoreClustering(
+ this ISiloBuilder builder) where TDbContext : ClusterDbContext
{
return builder
.ConfigureServices(services =>
{
- services.AddSingleton>();
+ services
+ .AddSingleton>();
});
}
@@ -66,21 +67,21 @@ public static ISiloBuilder UseEntityFrameworkCoreClustering(
///
/// The provided .
///
- public static IClientBuilder UseEntityFrameworkCoreClustering(
+ public static IClientBuilder UseEntityFrameworkCoreClustering(
this IClientBuilder builder,
- Action configureDatabase) where TDbContext : ClusterDbContext
+ Action configureDatabase) where TDbContext : ClusterDbContext
{
return builder
.ConfigureServices(services =>
{
services.AddPooledDbContextFactory(configureDatabase);
})
- .UseEntityFrameworkCoreClustering();
+ .UseEntityFrameworkCoreClustering();
}
///
/// Configures the silo to use Entity Framework Core for clustering.
- /// This overload expects a to be registered already.
+ /// This overload expects a to be registered already.
///
///
/// The silo builder.
@@ -88,13 +89,13 @@ public static IClientBuilder UseEntityFrameworkCoreClustering(
///
/// The provided .
///
- public static IClientBuilder UseEntityFrameworkCoreClustering(
- this IClientBuilder builder) where TDbContext : ClusterDbContext
+ public static IClientBuilder UseEntityFrameworkCoreClustering(
+ this IClientBuilder builder) where TDbContext : ClusterDbContext
{
return builder
.ConfigureServices(services =>
{
- services.AddSingleton>();
+ services.AddSingleton>();
});
}
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFGatewayListProvider.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFGatewayListProvider.cs
index e7e2973b9a..b439f7daf8 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFGatewayListProvider.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFGatewayListProvider.cs
@@ -13,7 +13,7 @@
namespace Orleans.Clustering.EntityFrameworkCore;
-internal class EFGatewayListProvider : IGatewayListProvider where TDbContext : ClusterDbContext
+internal class EFGatewayListProvider : IGatewayListProvider where TDbContext : ClusterDbContext
{
private readonly ILogger _logger;
private readonly string _clusterId;
@@ -29,7 +29,7 @@ public EFGatewayListProvider(
IOptions gatewayOptions,
IDbContextFactory dbContextFactory)
{
- this._logger = loggerFactory.CreateLogger>();
+ this._logger = loggerFactory.CreateLogger>();
this._clusterId = clusterOptions.Value.ClusterId;
this._dbContextFactory = dbContextFactory;
this.MaxStaleness = gatewayOptions.Value.GatewayListRefreshPeriod;
@@ -67,5 +67,5 @@ public async Task> GetGateways()
}
}
- private static Uri ConvertToGatewayUri(SiloRecord record) => SiloAddress.New(new IPEndPoint(IPAddress.Parse(record.Address), record.ProxyPort!.Value), record.Generation).ToGatewayUri();
+ private static Uri ConvertToGatewayUri(SiloRecord record) => SiloAddress.New(new IPEndPoint(IPAddress.Parse(record.Address), record.ProxyPort!.Value), record.Generation).ToGatewayUri();
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs
index 169b3da380..fe17ec9882 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/EFMembershipTable.cs
@@ -12,21 +12,24 @@
namespace Orleans.Clustering.EntityFrameworkCore;
-internal class EFMembershipTable : IMembershipTable where TDbContext : ClusterDbContext
+internal class EFMembershipTable : IMembershipTable where TDbContext : ClusterDbContext
{
private readonly ILogger _logger;
private readonly string _clusterId;
private readonly IDbContextFactory _dbContextFactory;
- private SiloRecord? _self;
+ private readonly IEFClusterETagConverter _etagConverter;
+ private SiloRecord? _self;
public EFMembershipTable(
ILoggerFactory loggerFactory,
IOptions clusterOptions,
- IDbContextFactory dbContextFactory)
+ IDbContextFactory dbContextFactory,
+ IEFClusterETagConverter etagConverter)
{
- this._logger = loggerFactory.CreateLogger>();
+ this._logger = loggerFactory.CreateLogger>();
this._clusterId = clusterOptions.Value.ClusterId;
this._dbContextFactory = dbContextFactory;
+ this._etagConverter = etagConverter;
}
public async Task InitializeMembershipTable(bool tryInitTableVersion)
@@ -44,7 +47,7 @@ public async Task InitializeMembershipTable(bool tryInitTableVersion)
if (record is not null) return;
- record = new ClusterRecord {Version = 0, Id = this._clusterId, Timestamp = DateTimeOffset.UtcNow};
+ record = new ClusterRecord {Version = 0, Id = this._clusterId, Timestamp = DateTimeOffset.UtcNow};
ctx.Clusters.Add(record);
await ctx.SaveChangesAsync().ConfigureAwait(false);
@@ -97,7 +100,7 @@ public async Task CleanupDefunctSiloEntries(DateTimeOffset beforeDate)
var silos = await ctx.Silos
.Where(s =>
s.ClusterId == this._clusterId &&
- s.Status == SiloStatus.Dead &&
+ s.Status != SiloStatus.Active &&
s.IAmAliveTime < beforeDate)
.ToArrayAsync()
.ConfigureAwait(false);
@@ -122,7 +125,7 @@ public async Task ReadRow(SiloAddress key)
{
var ctx = this._dbContextFactory.CreateDbContext();
- var record = await ctx.Silos.Include(s => s.ClusterId).AsNoTracking()
+ var record = await ctx.Silos.Include(s => s.Cluster).AsNoTracking()
.SingleOrDefaultAsync(s =>
s.ClusterId == this._clusterId &&
s.Address == key.Endpoint.Address.ToString() &&
@@ -137,10 +140,10 @@ public async Task ReadRow(SiloAddress key)
var version = new TableVersion(
record.Cluster.Version,
- BitConverter.ToUInt64(record.ETag).ToString()
+ this._etagConverter.FromDbETag(record.Cluster.ETag)
);
- var memEntries = new List> {Tuple.Create(ConvertRecord(record), BitConverter.ToUInt64(record.ETag).ToString())};
+ var memEntries = new List> {Tuple.Create(ConvertRecord(record), this._etagConverter.FromDbETag(record.ETag))};
return new MembershipTableData(memEntries, version);
}
@@ -169,7 +172,7 @@ public async Task ReadAll()
var version = new TableVersion(
clusterRecord.Version,
- BitConverter.ToUInt64(clusterRecord.ETag).ToString()
+ this._etagConverter.FromDbETag(clusterRecord.ETag)
);
var memEntries = new List>();
@@ -178,7 +181,7 @@ public async Task ReadAll()
try
{
var membershipEntry = ConvertRecord(siloRecord);
- memEntries.Add(new Tuple(membershipEntry, BitConverter.ToUInt64(siloRecord.ETag).ToString()));
+ memEntries.Add(new Tuple(membershipEntry, this._etagConverter.FromDbETag(siloRecord.ETag)));
}
catch (Exception exc)
{
@@ -210,11 +213,12 @@ public async Task InsertRow(MembershipEntry entry, TableVersion tableVersi
ctx.Clusters.Update(clusterRecord);
ctx.Silos.Add(siloRecord);
- await ctx.SaveChangesAsync().ConfigureAwait(false);
+ var affected =await ctx.SaveChangesAsync().ConfigureAwait(false);
return true;
}
- catch (DbUpdateConcurrencyException)
+ catch (DbUpdateException exc)
{
+ this._logger.LogWarning(exc, "Failure inserting entry for cluster {Cluster}", this._clusterId);
return false;
}
catch (Exception exc)
@@ -231,15 +235,14 @@ public async Task UpdateRow(MembershipEntry entry, string etag, TableVersi
{
var clusterRecord = this.ConvertToRecord(tableVersion);
var siloRecord = this.ConvertToRecord(entry);
- siloRecord.ClusterId = clusterRecord.Id;
- siloRecord.ETag = BitConverter.GetBytes(ulong.Parse(etag));
+ siloRecord.ETag = this._etagConverter.ToDbETag(etag);
var ctx = this._dbContextFactory.CreateDbContext();
ctx.Clusters.Update(clusterRecord);
ctx.Silos.Update(siloRecord);
- await ctx.SaveChangesAsync().ConfigureAwait(false);
+ var affected = await ctx.SaveChangesAsync().ConfigureAwait(false);
return true;
}
catch (DbUpdateConcurrencyException)
@@ -294,7 +297,7 @@ public async Task UpdateIAmAlive(MembershipEntry entry)
}
}
- private static MembershipEntry ConvertRecord(in SiloRecord record)
+ private static MembershipEntry ConvertRecord(in SiloRecord record)
{
var entry = new MembershipEntry
{
@@ -329,9 +332,9 @@ private static MembershipEntry ConvertRecord(in SiloRecord record)
return entry;
}
- private SiloRecord ConvertToRecord(in MembershipEntry memEntry)
+ private SiloRecord ConvertToRecord(in MembershipEntry memEntry)
{
- var record = new SiloRecord
+ var record = new SiloRecord
{
ClusterId = this._clusterId,
Address = memEntry.SiloAddress.Endpoint.Address.ToString(),
@@ -359,8 +362,8 @@ private SiloRecord ConvertToRecord(in MembershipEntry memEntry)
return record;
}
- private ClusterRecord ConvertToRecord(in TableVersion tableVersion)
+ private ClusterRecord ConvertToRecord(in TableVersion tableVersion)
{
- return new() {Id = this._clusterId, Version = tableVersion.Version, Timestamp = DateTimeOffset.UtcNow, ETag = BitConverter.GetBytes(ulong.Parse(tableVersion.VersionEtag))};
+ return new() {Id = this._clusterId, Version = tableVersion.Version, Timestamp = DateTimeOffset.UtcNow, ETag = this._etagConverter.ToDbETag(tableVersion.VersionEtag)};
}
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/IEFClusterETagConverter.cs b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/IEFClusterETagConverter.cs
new file mode 100644
index 0000000000..a982357cda
--- /dev/null
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/IEFClusterETagConverter.cs
@@ -0,0 +1,8 @@
+namespace Orleans.Clustering.EntityFrameworkCore;
+
+public interface IEFClusterETagConverter
+{
+ TETag ToDbETag(string etag);
+
+ string FromDbETag(TETag etag);
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj
index 9ea83e0591..650952c12b 100644
--- a/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj
+++ b/src/EFCore/Orleans.Clustering.EntityFrameworkCore/Orleans.Clustering.EntityFrameworkCore.csproj
@@ -11,6 +11,8 @@
+
+
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Data/SqlServerGrainDirectoryDbContext.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Data/SqlServerGrainDirectoryDbContext.cs
index 873b7973a3..7225d4ae7b 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Data/SqlServerGrainDirectoryDbContext.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Data/SqlServerGrainDirectoryDbContext.cs
@@ -3,7 +3,7 @@
namespace Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.Data;
-public class SqlServerGrainDirectoryDbContext : GrainDirectoryDbContext
+public class SqlServerGrainDirectoryDbContext : GrainDirectoryDbContext
{
public SqlServerGrainDirectoryDbContext(DbContextOptions options) : base(options)
{
@@ -11,7 +11,7 @@ public SqlServerGrainDirectoryDbContext(DbContextOptions(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => new {p.ClusterId, p.GrainId}).IsClustered(false).HasName("PK_Activations");
c.Property(p => p.ClusterId).IsRequired();
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.csproj b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.csproj
index 91bdd2a457..2e0c9d3fbe 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.csproj
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer.csproj
@@ -14,6 +14,10 @@
Orleans.GrainDirectory.EntityFrameworkCore.SqlServer
+
+
+
+
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerGrainDirectoryETagConverter.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerGrainDirectoryETagConverter.cs
new file mode 100644
index 0000000000..fcc7afc2c4
--- /dev/null
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerGrainDirectoryETagConverter.cs
@@ -0,0 +1,11 @@
+using System;
+using Orleans.GrainDirectory.EntityFrameworkCore;
+
+namespace Orleans.GrainDirectory;
+
+internal class SqlServerGrainDirectoryETagConverter : IEFGrainDirectoryETagConverter
+{
+ public byte[] ToDbETag(string etag) => BitConverter.GetBytes(ulong.Parse(etag));
+
+ public string FromDbETag(byte[] etag) => BitConverter.ToUInt64(etag).ToString();
+}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
index 97b1e360f9..fc9aa97c6c 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore.SqlServer/SqlServerHostingExtensions.cs
@@ -55,7 +55,8 @@ internal static IServiceCollection AddEntityFrameworkCoreSqlServerGrainDirectory
string name)
{
services
- .AddSingletonNamedService(name, (sp, _) => ActivatorUtilities.CreateInstance>(sp))
+ .AddSingleton, SqlServerGrainDirectoryETagConverter>()
+ .AddSingletonNamedService(name, (sp, _) => ActivatorUtilities.CreateInstance>(sp))
.AddSingletonNamedService>(name, (s, n) => (ILifecycleParticipant)s.GetRequiredServiceByName(n));
return services;
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainActivationRecord.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainActivationRecord.cs
index 085cb4a4ae..fc7e5e62d5 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainActivationRecord.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainActivationRecord.cs
@@ -1,13 +1,11 @@
-using System;
-
namespace Orleans.GrainDirectory.EntityFrameworkCore.Data;
-public class GrainActivationRecord
+public class GrainActivationRecord
{
public string ClusterId { get; set; } = default!;
public string GrainId { get; set; } = default!;
public string SiloAddress { get; set; } = default!;
public string ActivationId { get; set; } = default!;
public long MembershipVersion { get; set; }
- public byte[] ETag { get; set; } = Array.Empty();
+ public TETag ETag { get; set; } = default!;
}
\ No newline at end of file
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainDirectoryDbContext.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainDirectoryDbContext.cs
index dfa77ce2ec..62b0f40cff 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainDirectoryDbContext.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/Data/GrainDirectoryDbContext.cs
@@ -2,9 +2,9 @@
namespace Orleans.GrainDirectory.EntityFrameworkCore.Data;
-public class GrainDirectoryDbContext : DbContext where TDbContext : DbContext
+public class GrainDirectoryDbContext : DbContext where TDbContext : DbContext
{
- public DbSet Activations { get; set; } = default!;
+ public DbSet> Activations { get; set; } = default!;
public GrainDirectoryDbContext(DbContextOptions options) : base(options)
{
@@ -12,7 +12,7 @@ public GrainDirectoryDbContext(DbContextOptions options) : base(opti
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
- modelBuilder.Entity(c =>
+ modelBuilder.Entity>(c =>
{
c.HasKey(p => new {p.ClusterId, p.GrainId});
c.Property(p => p.ClusterId).IsRequired();
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs
index 2610b8b8e0..d71fa96b8f 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFCoreGrainDirectory.cs
@@ -12,20 +12,23 @@
namespace Orleans.GrainDirectory.EntityFrameworkCore;
-public class EFCoreGrainDirectory : IGrainDirectory, ILifecycleParticipant where TDbContext : GrainDirectoryDbContext
+public class EFCoreGrainDirectory : IGrainDirectory, ILifecycleParticipant where TDbContext : GrainDirectoryDbContext
{
private readonly ILogger _logger;
private readonly IDbContextFactory _dbContextFactory;
+ private readonly IEFGrainDirectoryETagConverter _eTagConverter;
private readonly string _clusterId;
public EFCoreGrainDirectory(
ILoggerFactory loggerFactory,
IDbContextFactory dbContextFactory,
- IOptions clusterOptions)
+ IOptions clusterOptions,
+ IEFGrainDirectoryETagConverter eTagConverter)
{
- this._logger = loggerFactory.CreateLogger>();
+ this._logger = loggerFactory.CreateLogger>();
this._dbContextFactory = dbContextFactory;
this._clusterId = clusterOptions.Value.ClusterId;
+ this._eTagConverter = eTagConverter;
}
public Task Register(GrainAddress address) => this.Register(address, null);
@@ -47,14 +50,14 @@ public EFCoreGrainDirectory(
c.GrainId == grainIdStr)
.ConfigureAwait(false);
- var previousRecord = this.FromGrainAddress(previousAddress);
+ var previousEntry = this.FromGrainAddress(previousAddress);
if (record is null)
{
ctx.Activations.Add(toRegister);
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
- else if (record.ActivationId != previousRecord.ActivationId || record.SiloAddress != previousRecord.SiloAddress)
+ else if (record.ActivationId != previousEntry.ActivationId || record.SiloAddress != previousEntry.SiloAddress)
{
return await Lookup(address.GrainId).ConfigureAwait(false);
}
@@ -65,7 +68,7 @@ public EFCoreGrainDirectory(
ctx.Activations.Update(toRegister);
await ctx.SaveChangesAsync().ConfigureAwait(false);
- return address;
+ return this.ToGrainAddress(toRegister);
}
}
else
@@ -74,11 +77,10 @@ public EFCoreGrainDirectory(
await ctx.SaveChangesAsync().ConfigureAwait(false);
}
}
- catch (Exception exc)
+ catch
{
- this._logger.LogWarning(exc, "Unable to update Grain Directory");
- WrappedException.CreateAndRethrow(exc);
- throw;
+ // Possible race condition?
+ return await Lookup(address.GrainId).ConfigureAwait(false);
}
return await Lookup(address.GrainId).ConfigureAwait(false);
@@ -195,19 +197,19 @@ private Task InitializeIfNeeded(CancellationToken ct = default)
public void Participate(ISiloLifecycle lifecycle)
{
- lifecycle.Subscribe(nameof(EFCoreGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, InitializeIfNeeded);
+ lifecycle.Subscribe(nameof(EFCoreGrainDirectory), ServiceLifecycleStage.RuntimeInitialize, InitializeIfNeeded);
}
- public GrainAddress ToGrainAddress(GrainActivationRecord record)
+ public GrainAddress ToGrainAddress(GrainActivationRecord record)
{
return new GrainAddress {GrainId = GrainId.Parse(record.GrainId), SiloAddress = SiloAddress.FromParsableString(record.SiloAddress), ActivationId = ActivationId.FromParsableString(record.ActivationId), MembershipVersion = new MembershipVersion(record.MembershipVersion)};
}
- private GrainActivationRecord FromGrainAddress(GrainAddress address)
+ private GrainActivationRecord FromGrainAddress(GrainAddress address)
{
ArgumentNullException.ThrowIfNull(address.SiloAddress);
- return new GrainActivationRecord
+ return new GrainActivationRecord
{
ClusterId = this._clusterId,
GrainId = address.GrainId.ToString(),
diff --git a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFGrainDirectoryHostingExtension.cs b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFGrainDirectoryHostingExtension.cs
index 9527563e23..1ea61e3c41 100644
--- a/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFGrainDirectoryHostingExtension.cs
+++ b/src/EFCore/Orleans.GrainDirectory.EntityFrameworkCore/EFGrainDirectoryHostingExtension.cs
@@ -10,52 +10,52 @@ namespace Orleans.GrainDirectory;
public static class EFGrainDirectoryHostingExtension
{
- public static ISiloBuilder UseEntityFrameworkCoreGrainDirectoryAsDefault(
+ public static ISiloBuilder UseEntityFrameworkCoreGrainDirectoryAsDefault(
this ISiloBuilder builder,
- Action configureDatabase) where TDbContext : GrainDirectoryDbContext
+ Action configureDatabase) where TDbContext : GrainDirectoryDbContext
{
- return builder.ConfigureServices(services => services.AddEntityFrameworkCoreGrainDirectory(GrainDirectoryAttribute.DEFAULT_GRAIN_DIRECTORY, configureDatabase));
+ return builder.ConfigureServices(services => services.AddEntityFrameworkCoreGrainDirectory(GrainDirectoryAttribute.DEFAULT_GRAIN_DIRECTORY, configureDatabase));
}
- public static ISiloBuilder UseEntityFrameworkCoreGrainDirectoryAsDefault(
- this ISiloBuilder builder) where TDbContext : GrainDirectoryDbContext
+ public static ISiloBuilder UseEntityFrameworkCoreGrainDirectoryAsDefault(
+ this ISiloBuilder builder) where TDbContext : GrainDirectoryDbContext
{
- return builder.ConfigureServices(services => services.AddEntityFrameworkCoreGrainDirectory(GrainDirectoryAttribute.DEFAULT_GRAIN_DIRECTORY));
+ return builder.ConfigureServices(services => services.AddEntityFrameworkCoreGrainDirectory