前回はSQLite3のデータをWeb APIで操作してみましたが、今回はマルチマスター構成でレプリケーションしてみようと思います。
https://qiita.com/namikitakeo/items/42fa3e42c2c956029bb0
RaspberryPi_1(マスター1)
GETリクエストはそのままで、POST/PUT/DELETEリクエストAPIをブロックとして他ノードに転送します。
RaspberryPi_2(マスター2)
GETリクエストはそのままで、POST/PUT/DELETEリクエストAPIをブロックとして他ノードに転送します。
WEBAPI起動時に、他ノードからAPIブロックチェーンを取得します。取得失敗した場合は、GENESISブロックを生成します。
POST/PUT/DELETEリクエストAPIをブロックとして、ブロックチェーンに追加します。オンメモリーで改竄がむずかしいため、PreviousHashは前ブロックのIdとします。
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!;
}
}
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();
appsettings.json
{
"PizzaStoreSetting": {
"BaseUrl": "http://RaspberryPi_2:5033"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
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"
}
}
}
}