diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 00000000..337014b2
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "5.0.10",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/Emulsion.Database/Emulsion.Database.fsproj b/Emulsion.Database/Emulsion.Database.fsproj
new file mode 100644
index 00000000..55f084a5
--- /dev/null
+++ b/Emulsion.Database/Emulsion.Database.fsproj
@@ -0,0 +1,23 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/Emulsion.Database/EmulsionDbContext.fs b/Emulsion.Database/EmulsionDbContext.fs
new file mode 100644
index 00000000..8d26a1f3
--- /dev/null
+++ b/Emulsion.Database/EmulsionDbContext.fs
@@ -0,0 +1,20 @@
+namespace Emulsion.Database
+
+open Emulsion.Database.Models
+open Microsoft.EntityFrameworkCore
+open Microsoft.EntityFrameworkCore.Design
+
+type EmulsionDbContext(dataSource: string) =
+ inherit DbContext()
+
+ [] val mutable telegramContents: DbSet
+ member this.TelegramContents with get() = this.telegramContents and set v = this.telegramContents <- v
+
+ override _.OnConfiguring options =
+ options.UseSqlite($"Data Source={dataSource};") |> ignore
+
+/// This type is used by the EFCore infrastructure when creating a new migration.
+type EmulsionDbContextDesignFactory() =
+ interface IDesignTimeDbContextFactory with
+ member this.CreateDbContext _ =
+ new EmulsionDbContext(":memory:")
diff --git a/Emulsion.Database/Initializer.fs b/Emulsion.Database/Initializer.fs
new file mode 100644
index 00000000..cfa1bb2e
--- /dev/null
+++ b/Emulsion.Database/Initializer.fs
@@ -0,0 +1,7 @@
+module Emulsion.Database.Initializer
+
+open Microsoft.EntityFrameworkCore
+
+let initializeDatabase(context: EmulsionDbContext): Async = async {
+ do! Async.AwaitTask(context.Database.MigrateAsync())
+}
diff --git a/Emulsion.Database/Migrations/20210926114410_Initialize.fs b/Emulsion.Database/Migrations/20210926114410_Initialize.fs
new file mode 100644
index 00000000..14d1dbfd
--- /dev/null
+++ b/Emulsion.Database/Migrations/20210926114410_Initialize.fs
@@ -0,0 +1,58 @@
+//
+namespace Emulsion.Database.Migrations
+
+open System
+open Emulsion.Database
+open Microsoft.EntityFrameworkCore
+open Microsoft.EntityFrameworkCore.Infrastructure
+open Microsoft.EntityFrameworkCore.Metadata
+open Microsoft.EntityFrameworkCore.Migrations
+open Microsoft.EntityFrameworkCore.Storage.ValueConversion
+
+[)>]
+[]
+type Initialize() =
+ inherit Migration()
+
+ override this.Up(migrationBuilder:MigrationBuilder) =
+ migrationBuilder.CreateTable(
+ name = "TelegramContents"
+ ,columns = (fun table ->
+ {|
+ Id =
+ table.Column(
+ nullable = false
+ ,``type`` = "TEXT"
+ )
+ |})
+ ,constraints =
+ (fun table ->
+ table.PrimaryKey("PK_TelegramContents", (fun x -> (x.Id) :> obj)) |> ignore
+ )
+ ) |> ignore
+
+
+ override this.Down(migrationBuilder:MigrationBuilder) =
+ migrationBuilder.DropTable(
+ name = "TelegramContents"
+ ) |> ignore
+
+
+ override this.BuildTargetModel(modelBuilder: ModelBuilder) =
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.10")
+ |> ignore
+
+ modelBuilder.Entity("Emulsion.Database.Models.TelegramContent", (fun b ->
+
+ b.Property("Id")
+ .IsRequired(true)
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT") |> ignore
+
+ b.HasKey("Id") |> ignore
+
+ b.ToTable("TelegramContents") |> ignore
+
+ )) |> ignore
+
diff --git a/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs b/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs
new file mode 100644
index 00000000..1262c8cd
--- /dev/null
+++ b/Emulsion.Database/Migrations/EmulsionDbContextModelSnapshot.fs
@@ -0,0 +1,33 @@
+//
+namespace Emulsion.Database.Migrations
+
+open System
+open Emulsion.Database
+open Microsoft.EntityFrameworkCore
+open Microsoft.EntityFrameworkCore.Infrastructure
+open Microsoft.EntityFrameworkCore.Metadata
+open Microsoft.EntityFrameworkCore.Migrations
+open Microsoft.EntityFrameworkCore.Storage.ValueConversion
+
+[)>]
+type EmulsionDbContextModelSnapshot() =
+ inherit ModelSnapshot()
+
+ override this.BuildModel(modelBuilder: ModelBuilder) =
+ modelBuilder
+ .HasAnnotation("ProductVersion", "5.0.10")
+ |> ignore
+
+ modelBuilder.Entity("Emulsion.Database.Models.TelegramContent", (fun b ->
+
+ b.Property("Id")
+ .IsRequired(true)
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT") |> ignore
+
+ b.HasKey("Id") |> ignore
+
+ b.ToTable("TelegramContents") |> ignore
+
+ )) |> ignore
+
diff --git a/Emulsion.Database/Models.fs b/Emulsion.Database/Models.fs
new file mode 100644
index 00000000..286b068a
--- /dev/null
+++ b/Emulsion.Database/Models.fs
@@ -0,0 +1,9 @@
+namespace Emulsion.Database.Models
+
+open System
+open System.ComponentModel.DataAnnotations
+
+[]
+type TelegramContent = {
+ [] Id: Guid
+}
diff --git a/Emulsion.Tests/Database/InitializerTests.fs b/Emulsion.Tests/Database/InitializerTests.fs
new file mode 100644
index 00000000..eef823e9
--- /dev/null
+++ b/Emulsion.Tests/Database/InitializerTests.fs
@@ -0,0 +1,14 @@
+module Emulsion.Tests.Database.InitializerTests
+
+open System.IO
+open Emulsion.Database
+open Xunit
+
+[]
+let ``Database initialization``(): unit =
+ async {
+ let databasePath = Path.Combine(Path.GetTempPath(), "emulsion-test.db")
+ use context = new EmulsionDbContext(databasePath)
+ let! _ = Async.AwaitTask(context.Database.EnsureDeletedAsync())
+ do! Initializer.initializeDatabase context
+ } |> Async.RunSynchronously
diff --git a/Emulsion.Tests/Emulsion.Tests.fsproj b/Emulsion.Tests/Emulsion.Tests.fsproj
index 039a2cc2..5cf2ff3a 100644
--- a/Emulsion.Tests/Emulsion.Tests.fsproj
+++ b/Emulsion.Tests/Emulsion.Tests.fsproj
@@ -26,6 +26,7 @@
+
@@ -37,5 +38,6 @@
+
\ No newline at end of file
diff --git a/Emulsion.sln b/Emulsion.sln
index 66f6e503..d5da6263 100644
--- a/Emulsion.sln
+++ b/Emulsion.sln
@@ -26,6 +26,18 @@ ProjectSection(SolutionItems) = preProject
.github\workflows\docker.yml = .github\workflows\docker.yml
EndProjectSection
EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Emulsion.Database", "Emulsion.Database\Emulsion.Database.fsproj", "{0111F688-1AE2-4B5D-BF46-D60B64078788}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{D03C4F4A-9806-46AA-8654-14363DFB3FBE}"
+ProjectSection(SolutionItems) = preProject
+ .config\dotnet-tools.json = .config\dotnet-tools.json
+EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{12336258-82C7-4C09-A73F-8D0B146578B2}"
+ProjectSection(SolutionItems) = preProject
+ docs\create-migration.md = docs\create-migration.md
+EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -63,6 +75,18 @@ Global
{1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x64.Build.0 = Release|x64
{1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x86.ActiveCfg = Release|x86
{1B3B65DD-FD58-45FA-B6FF-8532B513A7D9}.Release|x86.Build.0 = Release|x86
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x64.Build.0 = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Debug|x86.Build.0 = Debug|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x64.ActiveCfg = Release|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x64.Build.0 = Release|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x86.ActiveCfg = Release|Any CPU
+ {0111F688-1AE2-4B5D-BF46-D60B64078788}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7D1ADF47-BF1C-4007-BB9B-08C283044467} = {973131E1-E645-4A50-A0D2-1886A1A8F0C6}
diff --git a/README.md b/README.md
index 924e9711..36a72f69 100644
--- a/README.md
+++ b/README.md
@@ -94,15 +94,22 @@ where `$EMULSION_VERSION` is the version of the image to publish.
Documentation
-------------
-- [Changelog][changelog]
-- [License (MIT)][license]
+Common documentation:
+
+- [Changelog][docs.changelog]
+- [License (MIT)][docs.license]
+
+Developer documentation:
+
+- [How to Create a Database Migration][docs.create-migration]
[andivionian-status-classifier]: https://github.com/ForNeVeR/andivionian-status-classifier#status-aquana-
-[changelog]: ./CHANGELOG.md
[docker-hub]: https://hub.docker.com/r/codingteam/emulsion
+[docs.changelog]: ./CHANGELOG.md
+[docs.create-migration]: ./docs/create-migration.md
+[docs.license]: ./LICENSE.md
[dotnet-runtime]: https://www.microsoft.com/net/download/core#/runtime
[dotnet-sdk]: https://www.microsoft.com/net/download/core
-[license]: ./LICENSE.md
[telegram]: https://telegram.org/
[xmpp]: https://xmpp.org/
diff --git a/docs/create-migration.md b/docs/create-migration.md
new file mode 100644
index 00000000..e689820f
--- /dev/null
+++ b/docs/create-migration.md
@@ -0,0 +1,15 @@
+How to Create a Database Migration
+==================================
+
+This article explains how to create a database migration using [EFCore.FSharp][efcore.fsharp].
+
+1. Change the entity type (see `Emulsion.Database/Models.fs`), update the `EmulsionDbContext` if required.
+2. Run the following shell commands:
+
+ ```console
+ $ dotnet tool restore
+ $ cd Emulsion.Database
+ $ dotnet ef migrations add
+ ```
+
+[efcore.fsharp]: https://github.com/efcore/EFCore.FSharp