Add PostgreSQL integration with EF Core + Notes demo page
This commit is contained in:
@@ -11,7 +11,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.App.Internal.Assets" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.DotNet.HotReload.WebAssembly.Browser" Version="10.0.103" />
|
||||||
|
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Sdk.WebAssembly.Pack" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -25,5 +25,11 @@
|
|||||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="notes">
|
||||||
|
<span class="bi bi-card-text" aria-hidden="true"></span> Notes (Postgres)
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
173
Freman.Sample.Web/Freman.Sample.Web/Components/Pages/Notes.razor
Normal file
173
Freman.Sample.Web/Freman.Sample.Web/Components/Pages/Notes.razor
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
@page "/notes"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using Freman.Sample.Web.Data
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@inject AppDbContext Db
|
||||||
|
|
||||||
|
<PageTitle>Notes</PageTitle>
|
||||||
|
|
||||||
|
<h1>Notes</h1>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
A tiny EF Core + PostgreSQL demo: type a note, save it, refresh, it’s still there.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (_isLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></div>
|
||||||
|
<span>Loading notes…</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_error))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
@_error
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<EditForm Model="_model" OnValidSubmit="SaveAsync">
|
||||||
|
<DataAnnotationsValidator/>
|
||||||
|
<ValidationSummary/>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="noteText">New note</label>
|
||||||
|
<input id="noteText"
|
||||||
|
class="form-control"
|
||||||
|
@bind-value="_model.Text"
|
||||||
|
@bind-value:event="oninput"
|
||||||
|
maxlength="500"
|
||||||
|
disabled="@(_isSaving || _isLoading)"/>
|
||||||
|
<div class="form-text">Max 500 characters.</div>
|
||||||
|
<ValidationMessage For="@(() => _model.Text)"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
disabled="@(_isSaving || _isLoading || string.IsNullOrWhiteSpace(_model.Text))">
|
||||||
|
@if (_isSaving)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Saving…</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Save</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-outline-secondary ms-2"
|
||||||
|
type="button"
|
||||||
|
@onclick="LoadAsync"
|
||||||
|
disabled="@(_isSaving || _isLoading)">
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_status))
|
||||||
|
{
|
||||||
|
<div class="mt-3 alert alert-info">@_status</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h2>Saved notes</h2>
|
||||||
|
|
||||||
|
@if (_notesList.Count == 0)
|
||||||
|
{
|
||||||
|
<p><em>No notes yet.</em></p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ul class="list-group">
|
||||||
|
@foreach (var n in _notesList)
|
||||||
|
{
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<span>@n.Text</span>
|
||||||
|
<small class="text-muted">@n.CreatedUtc.ToLocalTime()</small>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private sealed class NewNoteModel
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "Please enter some text.")]
|
||||||
|
[StringLength(500, ErrorMessage = "Notes are limited to 500 characters.")]
|
||||||
|
public string Text { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly NewNoteModel _model = new();
|
||||||
|
private bool _isSaving;
|
||||||
|
private bool _isLoading;
|
||||||
|
private string? _status;
|
||||||
|
private string? _error;
|
||||||
|
private List<Note> _notesList = [];
|
||||||
|
|
||||||
|
async protected override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
_status = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_notesList = await Db.Notes
|
||||||
|
.OrderByDescending(n => n.CreatedUtc)
|
||||||
|
.Take(50)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_error = $"Load failed: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAsync()
|
||||||
|
{
|
||||||
|
_isSaving = true;
|
||||||
|
_error = null;
|
||||||
|
_status = null;
|
||||||
|
|
||||||
|
var note = new Note
|
||||||
|
{
|
||||||
|
Text = _model.Text.Trim(),
|
||||||
|
CreatedUtc = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimistic UI: show it immediately
|
||||||
|
_notesList.Insert(0, note);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Db.Notes.Add(note);
|
||||||
|
await Db.SaveChangesAsync();
|
||||||
|
|
||||||
|
_model.Text = "";
|
||||||
|
_status = "Saved!";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Roll back optimistic insert
|
||||||
|
_notesList.Remove(note);
|
||||||
|
_error = $"Save failed: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Freman.Sample.Web/Freman.Sample.Web/Data/AppDbContext.cs
Normal file
22
Freman.Sample.Web/Freman.Sample.Web/Data/AppDbContext.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Freman.Sample.Web.Data;
|
||||||
|
|
||||||
|
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<Note> Notes => Set<Note>();
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Note>(b =>
|
||||||
|
{
|
||||||
|
b.ToTable("notes");
|
||||||
|
b.HasKey(x => x.Id);
|
||||||
|
b.Property(x => x.Text).HasMaxLength(500).IsRequired();
|
||||||
|
b.Property(x => x.CreatedUtc).IsRequired();
|
||||||
|
b.HasIndex(x => x.CreatedUtc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Freman.Sample.Web.Data;
|
||||||
|
|
||||||
|
public static class DbMigrationHelper
|
||||||
|
{
|
||||||
|
public static async Task ApplyMigrationsAsync(IServiceProvider services, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var scope = services.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
await db.Database.MigrateAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Freman.Sample.Web/Freman.Sample.Web/Data/Migrations/20260222193339_InitialCreate.Designer.cs
generated
Normal file
53
Freman.Sample.Web/Freman.Sample.Web/Data/Migrations/20260222193339_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Freman.Sample.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Freman.Sample.Web.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260222193339_InitialCreate")]
|
||||||
|
partial class InitialCreate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Freman.Sample.Web.Data.Note", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedUtc");
|
||||||
|
|
||||||
|
b.ToTable("notes", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Freman.Sample.Web.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "notes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Text = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||||
|
CreatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_notes", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_notes_CreatedUtc",
|
||||||
|
table: "notes",
|
||||||
|
column: "CreatedUtc");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "notes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Freman.Sample.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Freman.Sample.Web.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.3")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Freman.Sample.Web.Data.Note", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedUtc");
|
||||||
|
|
||||||
|
b.ToTable("notes", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Freman.Sample.Web/Freman.Sample.Web/Data/Note.cs
Normal file
8
Freman.Sample.Web/Freman.Sample.Web/Data/Note.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Freman.Sample.Web.Data;
|
||||||
|
|
||||||
|
public sealed class Note
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -10,13 +10,21 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Freman.Sample.Web.Client\Freman.Sample.Web.Client.csproj"/>
|
<ProjectReference Include="..\Freman.Sample.Web.Client\Freman.Sample.Web.Client.csproj"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.App.Internal.Assets" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.3"/>
|
||||||
|
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3"/>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\.dockerignore">
|
<Content Include="..\..\.dockerignore">
|
||||||
<Link>.dockerignore</Link>
|
<Link>.dockerignore</Link>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Freman.Sample.Web.Client.Pages;
|
using Freman.Sample.Web.Client.Pages;
|
||||||
using Freman.Sample.Web.Components;
|
using Freman.Sample.Web.Components;
|
||||||
|
using Freman.Sample.Web.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -8,11 +10,20 @@ builder.Services.AddRazorComponents()
|
|||||||
.AddInteractiveServerComponents()
|
.AddInteractiveServerComponents()
|
||||||
.AddInteractiveWebAssemblyComponents();
|
.AddInteractiveWebAssemblyComponents();
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
{
|
||||||
|
var cs = builder.Configuration.GetConnectionString("AppDb");
|
||||||
|
options.UseNpgsql(cs);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
|
// Auto-apply migrations for dev/demo convenience.
|
||||||
|
await DbMigrationHelper.ApplyMigrationsAsync(app.Services);
|
||||||
app.UseWebAssemblyDebugging();
|
app.UseWebAssemblyDebugging();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -4,5 +4,8 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"AppDb": "Host=localhost;Port=5432;Database=freman_sample;Username=appuser;Password=CHANGE_ME"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
README.md
Normal file
47
README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Freman.Sample.Web – Blazor + PostgreSQL (Docker) sample
|
||||||
|
|
||||||
|
This repo is a small demo for C# developers (e.g., Unity folks) who are new to web dev.
|
||||||
|
|
||||||
|
## Prereqs
|
||||||
|
|
||||||
|
- Docker Desktop
|
||||||
|
- .NET SDK (matching the repo)
|
||||||
|
- (Optional) EF tooling: `dotnet-ef`
|
||||||
|
|
||||||
|
## Quickstart (recommended)
|
||||||
|
|
||||||
|
1) Create a `.env` file next to `compose.yaml`:
|
||||||
|
```text POSTGRES_PASSWORD=your_local_dev_password```
|
||||||
|
|
||||||
|
2) Start everything:
|
||||||
|
```powershell docker compose up --build```
|
||||||
|
|
||||||
|
3) Open the app, then navigate to **Notes (Postgres)**.
|
||||||
|
Type a note, click **Save**, refresh the page — it should persist.
|
||||||
|
|
||||||
|
## Running from Rider (server on host, DB in Docker)
|
||||||
|
|
||||||
|
Start just Postgres:
|
||||||
|
|
||||||
|
```powershell docker compose up postgres```
|
||||||
|
|
||||||
|
Then run the `Freman.Sample.Web` project from Rider.
|
||||||
|
|
||||||
|
> In this mode, the app uses `appsettings.Development.json` which points to `localhost:5432`.
|
||||||
|
|
||||||
|
## Migrations (EF Core)
|
||||||
|
|
||||||
|
Install EF CLI once:
|
||||||
|
```powershell dotnet tool install --global dotnet-ef```
|
||||||
|
|
||||||
|
Add a migration:
|
||||||
|
|
||||||
|
```powershell dotnet ef migrations add InitialCreate --project .\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj --startup-project .\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj ` --output-dir Data\Migrations```
|
||||||
|
|
||||||
|
Apply it:
|
||||||
|
```powershell dotnet ef database update --project .\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj --startup-project .\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **DB auth failed**: check `.env` password matches the connection string in `compose.yaml`.
|
||||||
|
- **Port 5432 already in use**: stop the other Postgres instance or change the port mapping in `compose.yaml`.
|
||||||
26
compose.yaml
26
compose.yaml
@@ -1,9 +1,33 @@
|
|||||||
services:
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: freman.sample.postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: freman_sample
|
||||||
|
POSTGRES_USER: appuser
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-CHANGE_ME}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- freman_sample_pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U appuser -d freman_sample" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
freman.sample.web:
|
freman.sample.web:
|
||||||
image: freman.sample.web
|
image: freman.sample.web
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Freman.Sample.Web/Freman.Sample.Web/Dockerfile
|
dockerfile: Freman.Sample.Web/Freman.Sample.Web/Dockerfile
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
ASPNETCORE_ENVIRONMENT: Development
|
||||||
|
ConnectionStrings__AppDb: Host=postgres;Port=5432;Database=freman_sample;Username=appuser;Password=${POSTGRES_PASSWORD:-CHANGE_ME}
|
||||||
|
|
||||||
|
|
||||||
freman.sample.web.client:
|
freman.sample.web.client:
|
||||||
image: freman.sample.web.client
|
image: freman.sample.web.client
|
||||||
@@ -11,3 +35,5 @@
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Freman.Sample.Web/Freman.Sample.Web.Client/Dockerfile
|
dockerfile: Freman.Sample.Web/Freman.Sample.Web.Client/Dockerfile
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
freman_sample_pgdata:
|
||||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user