SQLite3のデータをマルチマスター構成でレプリケーションしてみる

前回はSQLite3のデータをWeb APIで操作してみましたが、今回はマルチマスター構成でレプリケーションしてみようと思います。
https://qiita.com/namikitakeo/items/42fa3e42c2c956029bb0

RaspberryPi_1（マスター１）
GETリクエストはそのままで、POST/PUT/DELETEリクエストAPIをブロックとして他ノードに転送します。

RaspberryPi_2（マスター２）
GETリクエストはそのままで、POST/PUT/DELETEリクエストAPIをブロックとして他ノードに転送します。

WEBAPI起動時に、他ノードからAPIブロックチェーンを取得します。取得失敗した場合は、GENESISブロックを生成します。
POST/PUT/DELETEリクエストAPIをブロックとして、ブロックチェーンに追加します。オンメモリーで改竄がむずかしいため、PreviousHashは前ブロックのIdとします。

```C#:Models/Pizza.cs
using Microsoft.EntityFrameworkCore;

namespace PizzaStore.Models
{
    public class PizzaStoreSetting
    {
        public string BaseUrl { get; set; }
    }
    public class PizzaChain
    {
        public string Id { get; set; }
        public string PreviousHash { get; set; }
        public string Timestamp { get; set; }
        public string Block { get; set; }
    }
    public class PizzaRequest
    {
        public string id { get; set; }
        public string name { get; set; }
        public string description { get; set; }
    }
    public class Pizza
    {
        public string? Id { get; set; }
        public string? Name { get; set; }
        public string? Description { get; set; }
    }
    class PizzaDb : DbContext
    {
        public PizzaDb(DbContextOptions options) : base(options) { }
        public DbSet<Pizza> Pizzas { get; set; } = null!;
    }
}
```

```C#:Program.cs
using PizzaStore.Models;
using System.Net.Http.Headers;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

// GENESIS
string phash = "19990101000000";
List<PizzaChain> pizzal = new List<PizzaChain>();
// RESTORE
var builder = WebApplication.CreateBuilder(args);
var settings = builder.Configuration.GetSection("PizzaStoreSetting").Get<PizzaStoreSetting>();
HttpClient client = new HttpClient();
try {
    if (settings is null) {
        pizzal.Add(new PizzaChain {Id = phash , PreviousHash = "00000101000000", Timestamp = "1999/01/01 00:00:00", Block = "GENESIS"});
    } else {
        client.BaseAddress = new Uri(settings.BaseUrl);
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(
"application/json"));
        pizzal = await client.GetFromJsonAsync<List<PizzaChain>>("/blocks");
        phash = pizzal[pizzal.Count-1].Id;
    }
} catch(Exception e) {
    pizzal.Add(new PizzaChain {Id = phash , PreviousHash = "00000101000000", Timestamp = "1999/01/01 00:00:00", Block = "GENESIS"});
}
// EF WEB
var connectionString = builder.Configuration.GetConnectionString("Pizzas") ?? "Data Source=Pizzas.db";
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSqlite<PizzaDb>(connectionString);
var app = builder.Build();
// INSERT
app.MapPost("/pizza", async (PizzaDb db, Pizza pizza) =>
{
    PizzaRequest body = new PizzaRequest();
    body.id = pizza.Id;
    body.name = pizza.Name;
    body.description = pizza.Description;
    DateTime dt = DateTime.Now;
    PizzaChain pizzac = new PizzaChain {Id =  dt.ToString("yyyyMMddHHmmss"), PreviousHash = phash, Timestamp = dt.ToString("yyyy/MM/dd HH:mm:ss"), Block = "POST /pizza [id="+body.id+",name="+body.name+",description="+body.description+"]"};
    phash = dt.ToString("yyyyMMddHHmmss");
    if (settings is  null) {
        await db.Pizzas.AddAsync(pizza);
        await db.SaveChangesAsync();
        pizzal.Add(pizzac);
    } else {
        HttpClient client = new HttpClient();
        try {
            client.BaseAddress = new Uri(settings.BaseUrl);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            HttpResponseMessage response = await client.PostAsJsonAsync("/block", pizzac);
            pizzal.Add(pizzac);
            await db.Pizzas.AddAsync(pizza);
            await db.SaveChangesAsync();
        } catch(Exception e) {
            Console.WriteLine("POST NG");
        }
    }
    return Results.Created($"/pizza/{pizza.Id}", pizza);
});
// UPDATE
app.MapPut("/pizza/{id}", async (PizzaDb db, Pizza updatepizza, string id) =>
{
    var pizza = await db.Pizzas.FindAsync(id);
    if (pizza is null) return Results.NotFound();
    PizzaRequest body = new PizzaRequest();
    body.name = updatepizza.Name;
    body.description = updatepizza.Description;
    DateTime dt = DateTime.Now;
    PizzaChain pizzac = new PizzaChain {Id =  dt.ToString("yyyyMMddHHmmss"), PreviousHash = phash, Timestamp = dt.ToString("yyyy/MM/dd HH:mm:ss"), Block = "PUT /pizza/"+id+" [name="+body.name+",description="+body.description+"]"};
    phash = dt.ToString("yyyyMMddHHmmss");
    if (settings is  null) {
        pizza.Name = updatepizza.Name;
        pizza.Description = updatepizza.Description;
        await db.SaveChangesAsync();
        pizzal.Add(pizzac);
    } else {
        HttpClient client = new HttpClient();
        try {
            client.BaseAddress = new Uri(settings.BaseUrl);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            HttpResponseMessage response = await client.PostAsJsonAsync("/block", pizzac);
            pizza.Name = updatepizza.Name;
            pizza.Description = updatepizza.Description;
            await db.SaveChangesAsync();
            pizzal.Add(pizzac);
        } catch (Exception e) {
            Console.WriteLine("PUT NG");
        }
    }
    return Results.NoContent();
});
// DELETE
app.MapDelete("/pizza/{id}", async (PizzaDb db, string id) =>
{
    var pizza = await db.Pizzas.FindAsync(id);
    if (pizza is null)
    {
       return Results.NotFound();
    }
    DateTime dt = DateTime.Now;
    PizzaChain pizzac = new PizzaChain {Id =  dt.ToString("yyyyMMddHHmmss"), PreviousHash = phash, Timestamp = dt.ToString("yyyy/MM/dd HH:mm:ss"), Block = "DELETE /pizza/"+id};
    phash = dt.ToString("yyyyMMddHHmmss");
    if (settings is  null) {
        db.Pizzas.Remove(pizza);
        await db.SaveChangesAsync();
        pizzal.Add(pizzac);
    } else {
        HttpClient client = new HttpClient();
        try {
            client.BaseAddress = new Uri(settings.BaseUrl);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            HttpResponseMessage response = await client.PostAsJsonAsync("/block", pizzac);
            db.Pizzas.Remove(pizza);
            await db.SaveChangesAsync();
            pizzal.Add(pizzac);
        } catch(Exception e) {
            Console.WriteLine("DELETE NG");
        }
    }
    return Results.Ok();
});
// SELECT
app.MapGet("/pizza/{id}", async (PizzaDb db, string id) => Results.Json(await db.Pizzas.FindAsync(id), new JsonSerializerOptions { WriteIndented = true }));
// SELECT ALL
// app.MapGet("/pizzas", async (PizzaDb db) => await db.Pizzas.ToListAsync());
app.MapGet("/pizzas", async (PizzaDb db) => Results.Json(await db.Pizzas.ToListAsync(), new JsonSerializerOptions { WriteIndented = true }));
// BLOCKS
app.MapGet("/blocks", async (PizzaDb db) =>
{
    return JsonSerializer.Serialize(pizzal, new JsonSerializerOptions { WriteIndented = true });
});
// BLOCK CHAIN
app.MapPost("/block", async (PizzaDb db, PizzaChain pizza) =>
{
    if (pizza.Block.StartsWith("POST ")) {
        int i = pizza.Block.IndexOf("id=");
        int n = pizza.Block.IndexOf("name=");
        int d = pizza.Block.IndexOf("description=");
        int l = pizza.Block.Length;
        string id = pizza.Block.Substring(i+3, n-i-4);
        string name = pizza.Block.Substring(n+5, d-n-6);
        string desc = pizza.Block.Substring(d+12, l-d-13);
        await db.Pizzas.AddAsync(new Pizza {Id = id, Name = name, Description = desc});
        await db.SaveChangesAsync();
        pizzal.Add(pizza);
        phash = pizza.Id;
    } else if (pizza.Block.StartsWith("PUT ")) {
        string id = pizza.Block.Substring(0, pizza.Block.IndexOf(" ["));
        id = id.Substring(id.Length-14);
        Console.WriteLine("|"+id+"|");
        int n = pizza.Block.IndexOf("name=");
        int d = pizza.Block.IndexOf("description=");
        int l = pizza.Block.Length;
        string name = pizza.Block.Substring(n+5, d-n-6);
        string desc = pizza.Block.Substring(d+12, l-d-13);
        var temp = await db.Pizzas.FindAsync(id);
        if (temp is not null) {
            temp.Name = name;
            temp.Description = desc;
            await db.SaveChangesAsync();
        }
        pizzal.Add(pizza);
        phash = pizza.Id;
    } else if (pizza.Block.StartsWith("DELETE ")) {
        string id = pizza.Block.Substring(pizza.Block.Length-14);
        var temp = await db.Pizzas.FindAsync(id);
        if (temp is not null)
        {
            db.Pizzas.Remove(temp);
            await db.SaveChangesAsync();
        }
        pizzal.Add(pizza);
        phash = pizza.Id;
    }
});
app.Run();
```

```JSON:appsettings.json
{
  "PizzaStoreSetting": {
    "BaseUrl": "http://RaspberryPi_2:5033"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
```

```JSON:Properties/launchSettings.json
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:19381",
      "sslPort": 44341
    }
  },
  "profiles": {
    "PizzaStore": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://RaspberryPi_1:7006;http://RaspberryPi_1:5033",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```