AspNetForge.WebCore 1.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package AspNetForge.WebCore --version 1.0.1
                    
NuGet\Install-Package AspNetForge.WebCore -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AspNetForge.WebCore" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AspNetForge.WebCore" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="AspNetForge.WebCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AspNetForge.WebCore --version 1.0.1
                    
#r "nuget: AspNetForge.WebCore, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AspNetForge.WebCore@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AspNetForge.WebCore&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=AspNetForge.WebCore&version=1.0.1
                    
Install as a Cake Tool

AspNetForge.WebCore

Class Library для ASP.NET Core Minimal API (.NET 8).

Что делает пакет

  • MapCrud<TEntity, TContext>(this WebApplication app, string route) — создаёт 5 стандартных эндпоинтов: GET, GET by id, POST, PUT, DELETE.
  • UseForgeResources(this WebApplication app, bool overwrite = false) — распаковывает встроенные JS/CSS/HTML в wwwroot приложения.
  • В комплект пакета входит полный пример проекта. При установке в целевой проект копируются Models.cs, Program.cs и wwwroot/.

Быстрый старт

1. Создайте проект

dotnet new web -n MyWebApp
cd MyWebApp

2. Установите пакет

dotnet add package AspNetForge.WebCore --version 1.0.1

3. Установите провайдер базы данных

Пакет не включает EF Core и PostgreSQL. Установите их самостоятельно:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11

Если нужны JWT или Identity, установите их отдельно:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.11
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 8.0.11

4. Соберите проект

В Visual Studio: Ctrl+Shift+B.

В консоли:

dotnet build

После сборки в корень проекта автоматически скопируются файлы примера:

Models.cs
Program.cs
wwwroot/
  index.html
  login.html
  register.html
  products.html
  partners.html
  producttypes.html
  materialtypes.html
  styles.css
  js/crud.js

appsettings.json копируется только если его ещё нет в проекте. Если файл уже был, добавьте строку подключения вручную (см. раздел 6).

5. Перезагрузите проект в Visual Studio

Visual Studio не всегда сразу показывает новые файлы. Правый клик по проекту → Перезагрузить проект (или закройте и откройте решение).

6. Настройте строку подключения

Откройте appsettings.json и добавьте:

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
  },
  "Logging": { ... },
  "AllowedHosts": "*"
}

Замените Username и Password на свои. База examdb должна существовать.

7. Создайте базу данных

В pgAdmin или psql:

CREATE DATABASE examdb;

Таблицы создадутся автоматически при первом обращении к API.

8. Запустите

dotnet run

Откройте http://localhost:5000 (или порт из launchSettings.json).


Если файлы не скопировались автоматически

Иногда Visual Studio не запускает build-targets пакета с первого раза. В этом случае создайте файлы вручную.

Models.cs

using Microsoft.EntityFrameworkCore;

namespace ExamProject.Models;

public class ProductType
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required decimal Coefficient { get; set; }
}

public class MaterialType
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required decimal PersentLoss { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required string Name { get; set; }
    public required string Article { get; set; }
    public required decimal MinCost { get; set; }
}

public class Partner
{
    public int Id { get; set; }
    public required string Type { get; set; }
    public required string Name { get; set; }
    public required string DirName { get; set; }
    public required string Email { get; set; }
    public required string Phone { get; set; }
    public required string Address { get; set; }
    public required string TIN { get; set; }
    public required int Rate { get; set; }
}

public class User
{
    public int Id { get; set; }
    public required string Login { get; set; }
    public required string Password { get; set; }
    public required string FullName { get; set; }
    public required string Role { get; set; }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<ProductType> ProductTypes { get; set; }
    public DbSet<MaterialType> MaterialTypes { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Partner> Partners { get; set; }
    public DbSet<User> Users { get; set; }
}

Program.cs

using AspNetForge.Web.Extensions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using ExamProject.Models;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddAuthorization();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login.html";
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseForgeResources(overwrite: true);
app.UseDefaultFiles();
app.UseStaticFiles();

app.MapGet("/", () => Results.Redirect("/index.html"));

app.MapCrud<ProductType, AppDbContext>("/api/producttypes");
app.MapCrud<MaterialType, AppDbContext>("/api/materialtypes");
app.MapCrud<Product, AppDbContext>("/api/products");
app.MapCrud<Partner, AppDbContext>("/api/partners");
app.MapCrud<User, AppDbContext>("/api/users");

app.MapPost("/login", async (AppDbContext db, HttpContext context) =>
{
    var form = context.Request.Form;
    var login = form["login"].ToString();
    var password = form["password"].ToString();

    var user = await db.Users.FirstOrDefaultAsync(u =>
        u.Login == login && u.Password == password);

    if (user is null)
        return Results.BadRequest("Логин или пароль неверный");

    var claims = new ClaimsIdentity(new[]
    {
        new Claim(ClaimsIdentity.DefaultNameClaimType, user.Login),
        new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role)
    }, "Cookies");

    await context.SignInAsync(new ClaimsPrincipal(claims));
    return Results.Redirect("/index.html");
});

app.MapPost("/register", async (AppDbContext db, HttpContext context) =>
{
    var form = context.Request.Form;
    var login = form["login"].ToString();
    var password = form["password"].ToString();

    if (password != form["confirmPassword"].ToString())
        return Results.BadRequest("Пароли не совпадают");

    if (await db.Users.AnyAsync(u => u.Login == login))
        return Results.BadRequest("Пользователь уже существует");

    await db.Users.AddAsync(new User
    {
        Login = login,
        Password = password,
        FullName = form["fullName"].ToString(),
        Role = "User"
    });

    await db.SaveChangesAsync();
    return Results.Redirect("/login.html");
});

app.MapGet("/logout", async (HttpContext context) =>
{
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return Results.Redirect("/login.html");
});

app.MapGet("/api/me", (HttpContext context) =>
{
    if (!context.User.Identity!.IsAuthenticated)
        return Results.Json(new { role = "Guest" });

    return Results.Json(new
    {
        role = context.User.FindFirst(ClaimsIdentity.DefaultRoleClaimType)?.Value ?? "User",
        name = context.User.Identity.Name
    });
});

app.Run();

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

wwwroot/styles.css

:root {
  --bg: #f9fafb;
  --text: #111827;
  --muted: #6b7280;
  --accent: #2563eb;
  --danger: #dc2626;
  --success: #16a34a;
  --card: #ffffff;
  --border: #e5e7eb;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
}

header {
  background: var(--card);
  border-bottom: 1px solid var(--border);
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

header h1 { margin: 0; font-size: 1.25rem; }

nav a {
  margin-left: 1rem;
  color: var(--accent);
  text-decoration: none;
}

main {
  max-width: 1200px;
  margin: 2rem auto;
  padding: 0 1rem;
}

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: .75rem;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}

input, select, button {
  padding: .5rem .75rem;
  border: 1px solid var(--border);
  border-radius: .5rem;
  font-size: 1rem;
}

button {
  background: var(--accent);
  color: white;
  border: none;
  cursor: pointer;
}

button:hover { opacity: .9; }
button.secondary { background: var(--muted); }
button.danger { background: var(--danger); }

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1rem;
}

td, th {
  padding: .75rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
}

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .75rem;
  margin-bottom: 1rem;
}

.auth-form {
  max-width: 360px;
  margin: 5rem auto;
}

.auth-form input {
  width: 100%;
  margin-bottom: .75rem;
}

.hidden { display: none; }

wwwroot/js/crud.js

const api = {
  async get(url) {
    const r = await fetch(url);
    if (!r.ok) throw new Error('GET ' + url + ' failed');
    return r.json();
  },
  async post(url, data) {
    const r = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!r.ok) throw new Error('POST failed');
    return r.json().catch(() => null);
  },
  async put(url, data) {
    const r = await fetch(url, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!r.ok) throw new Error('PUT failed');
    return r.json().catch(() => null);
  },
  async delete(url) {
    const r = await fetch(url, { method: 'DELETE' });
    if (!r.ok) throw new Error('DELETE failed');
  }
};

async function renderHeader() {
  try {
    const me = await api.get('/api/me');
    const authLink = document.getElementById('authLink');
    const userInfo = document.getElementById('userInfo');
    if (me.role !== 'Guest' && authLink) {
      authLink.textContent = 'Выйти';
      authLink.href = '/logout';
      if (userInfo) userInfo.textContent = me.name + ' (' + me.role + ')';
    }
  } catch { }
}

function renderTable(rows, allKeys, displayKeys, onEdit, onDelete) {
  const tbody = document.getElementById('tableBody');
  tbody.innerHTML = '';
  for (const row of rows) {
    const tr = document.createElement('tr');
    for (const key of displayKeys) {
      const td = document.createElement('td');
      td.textContent = row[key] ?? '';
      tr.appendChild(td);
    }
    const actions = document.createElement('td');
    const editBtn = document.createElement('button');
    editBtn.textContent = '✎';
    editBtn.onclick = () => onEdit(row);
    const delBtn = document.createElement('button');
    delBtn.textContent = '✕';
    delBtn.className = 'danger';
    delBtn.onclick = () => onDelete(row.id);
    actions.appendChild(editBtn);
    actions.appendChild(delBtn);
    tr.appendChild(actions);
    tbody.appendChild(tr);
  }
}

wwwroot/index.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Главная</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <h1>АспнетФордж</h1>
    <nav>
      <a href="/products.html">Товары</a>
      <a href="/partners.html">Партнеры</a>
      <a href="/producttypes.html">Типы товаров</a>
      <a href="/materialtypes.html">Типы материалов</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h2>Добро пожаловать</h2>
      <p>Это стартовый шаблон, созданный пакетом AspNetForge.WebCore.</p>
      <p><strong>Важно:</strong> перед запуском создайте базу данных PostgreSQL и обновите строку подключения в <code>appsettings.json</code>.</p>
    </div>
  </main>
  <script src="/js/crud.js"></script>
  <script>renderHeader();</script>
</body>
</html>

wwwroot/login.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Вход</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <main>
    <div class="card auth-form">
      <h2>Вход</h2>
      <form action="/login" method="post">
        <input name="login" placeholder="Логин" required>
        <input name="password" type="password" placeholder="Пароль" required>
        <button type="submit">Войти</button>
      </form>
      <p>Нет аккаунта? <a href="/register.html">Зарегистрироваться</a></p>
    </div>
  </main>
</body>
</html>

wwwroot/register.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Регистрация</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <main>
    <div class="card auth-form">
      <h2>Регистрация</h2>
      <form action="/register" method="post">
        <input name="login" placeholder="Логин" required>
        <input name="fullName" placeholder="ФИО" required>
        <input name="password" type="password" placeholder="Пароль" required>
        <input name="confirmPassword" type="password" placeholder="Повторите пароль" required>
        <button type="submit">Создать аккаунт</button>
      </form>
      <p>Уже есть аккаунт? <a href="/login.html">Войти</a></p>
    </div>
  </main>
</body>
</html>

wwwroot/products.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Товары</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <h1>Товары</h1>
    <nav>
      <a href="/index.html">Главная</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h3>Добавить / изменить</h3>
      <form id="productForm">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="type" name="type" placeholder="Тип" required>
          <input id="name" name="name" placeholder="Название" required>
          <input id="article" name="article" placeholder="Артикул" required>
          <input id="minCost" name="minCost" type="number" step="0.01" placeholder="Мин. стоимость" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>

    <div class="card">
      <table>
        <thead>
          <tr><th>Тип</th><th>Название</th><th>Артикул</th><th>Мин. стоимость</th><th></th></tr>
        </thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/products';
    const fields = ['id', 'type', 'name', 'article', 'minCost'];

    async function load() {
      const rows = await api.get(endpoint);
      renderTable(rows, fields, ['type','name','article','minCost'], editRow, deleteRow);
    }

    function editRow(row) {
      document.getElementById('id').value = row.id;
      document.getElementById('type').value = row.type;
      document.getElementById('name').value = row.name;
      document.getElementById('article').value = row.article;
      document.getElementById('minCost').value = row.minCost;
    }

    function resetForm() { document.getElementById('productForm').reset(); }

    document.getElementById('productForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {
        id: Number(document.getElementById('id').value) || 0,
        type: document.getElementById('type').value,
        name: document.getElementById('name').value,
        article: document.getElementById('article').value,
        minCost: Number(document.getElementById('minCost').value)
      };
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    renderHeader();
    load();
  </script>
</body>
</html>

wwwroot/partners.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Партнеры</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <h1>Партнеры</h1>
    <nav>
      <a href="/index.html">Главная</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h3>Добавить / изменить</h3>
      <form id="partnerForm">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="type" name="type" placeholder="Тип" required>
          <input id="name" name="name" placeholder="Название" required>
          <input id="dirName" name="dirName" placeholder="ФИО директора" required>
          <input id="email" name="email" type="email" placeholder="Email" required>
          <input id="phone" name="phone" placeholder="Телефон" required>
          <input id="address" name="address" placeholder="Адрес" required>
          <input id="tin" name="tin" placeholder="ИНН" required>
          <input id="rate" name="rate" type="number" placeholder="Рейтинг" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>

    <div class="card">
      <table>
        <thead>
          <tr><th>Тип</th><th>Название</th><th>Директор</th><th>Email</th><th>Телефон</th><th>Адрес</th><th>ИНН</th><th>Рейтинг</th><th></th></tr>
        </thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/partners';

    async function load() {
      const rows = await api.get(endpoint);
      renderTable(rows, ['id','type','name','dirName','email','phone','address','tin','rate'],
        ['type','name','dirName','email','phone','address','tin','rate'], editRow, deleteRow);
    }

    function editRow(row) {
      ['id','type','name','dirName','email','phone','address','tin','rate'].forEach(k => {
        document.getElementById(k).value = row[k];
      });
    }

    function resetForm() { document.getElementById('partnerForm').reset(); }

    document.getElementById('partnerForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {};
      ['id','type','name','dirName','email','phone','address','tin','rate'].forEach(k => {
        let v = document.getElementById(k).value;
        if (k === 'id' || k === 'rate') v = Number(v);
        payload[k] = v;
      });
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    renderHeader();
    load();
  </script>
</body>
</html>

wwwroot/producttypes.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Типы товаров</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <h1>Типы товаров</h1>
    <nav>
      <a href="/index.html">Главная</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h3>Добавить / изменить</h3>
      <form id="form">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="type" name="type" placeholder="Тип" required>
          <input id="coefficient" name="coefficient" type="number" step="0.01" placeholder="Коэффициент" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>
    <div class="card">
      <table>
        <thead><tr><th>Тип</th><th>Коэффициент</th><th></th></tr></thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/producttypes';
    const keys = ['id','type','coefficient'];

    async function load() {
      const rows = await api.get(endpoint);
      renderTable(rows, keys, ['type','coefficient'], editRow, deleteRow);
    }

    function editRow(row) {
      keys.forEach(k => document.getElementById(k).value = row[k]);
    }

    function resetForm() { document.getElementById('form').reset(); }

    document.getElementById('form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {
        id: Number(document.getElementById('id').value) || 0,
        type: document.getElementById('type').value,
        coefficient: Number(document.getElementById('coefficient').value)
      };
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    renderHeader();
    load();
  </script>
</body>
</html>

wwwroot/materialtypes.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Типы материалов</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <h1>Типы материалов</h1>
    <nav>
      <a href="/index.html">Главная</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h3>Добавить / изменить</h3>
      <form id="form">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="type" name="type" placeholder="Тип" required>
          <input id="persentLoss" name="persentLoss" type="number" step="0.01" placeholder="Процент потерь" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>
    <div class="card">
      <table>
        <thead><tr><th>Тип</th><th>% потерь</th><th></th></tr></thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/materialtypes';
    const keys = ['id','type','persentLoss'];

    async function load() {
      const rows = await api.get(endpoint);
      renderTable(rows, keys, ['type','persentLoss'], editRow, deleteRow);
    }

    function editRow(row) {
      keys.forEach(k => document.getElementById(k).value = row[k]);
    }

    function resetForm() { document.getElementById('form').reset(); }

    document.getElementById('form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {
        id: Number(document.getElementById('id').value) || 0,
        type: document.getElementById('type').value,
        persentLoss: Number(document.getElementById('persentLoss').value)
      };
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    renderHeader();
    load();
  </script>
</body>
</html>

Возможные ошибки и решения

Visual Studio предлагает запустить прошлый вариант

Это значит, что проект ещё не пересобран с новыми файлами.

  1. Нажмите Ctrl+Shift+B (собрать проект).
  2. Правый клик по проекту → Перезагрузить проект.
  3. Запустите снова.

CS0246: Не удалось найти тип или имя пространства имен "ExamProject"

Файл Models.cs не попал в компиляцию. Убедитесь, что он лежит в корне проекта. Если файла нет — создайте его вручную из раздела выше.

CS0234: Тип или имя пространства имен "EntityFrameworkCore" не существует

Не установлен пакет PostgreSQL:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11

Npgsql.PostgresException: 28P01: password authentication failed

Неверный логин или пароль в appsettings.json. Проверьте значения Username и Password.

Npgsql.PostgresException: 3D000: database "examdb" does not exist

База данных не создана. Создайте в pgAdmin/psql:

CREATE DATABASE examdb;

404 Not Found при открытии страницы

Убедитесь, что в Program.cs есть:

app.UseDefaultFiles();
app.UseStaticFiles();
app.MapGet("/", () => Results.Redirect("/index.html"));

А также что папка wwwroot существует и содержит index.html.

wwwroot пустой

При старте приложения UseForgeResources(overwrite: true) скопирует встроенные файлы в wwwroot. Если этого не произошло, проверьте, что вы вызвали метод до UseStaticFiles().


Сборка пакета из исходников

cd tmp/AspNetForge.Web
dotnet build -c Release

.nupkg появится в bin/Release/.

Структура пакета

lib/net8.0/AspNetForge.Web.dll
build/AspNetForge.WebCore.targets
contentFiles/any/any/Sample/Models.cs
contentFiles/any/any/Sample/Program.cs
contentFiles/any/any/Sample/appsettings.json
contentFiles/any/any/Sample/wwwroot/...

build/AspNetForge.WebCore.targets копирует файлы примера из пакета в целевой проект при сборке.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 128 6/15/2026
1.0.9 120 6/14/2026
1.0.8 116 6/14/2026
1.0.7 116 6/14/2026
1.0.6 112 6/14/2026
1.0.5 116 6/14/2026
1.0.4 114 6/14/2026
1.0.3 116 6/13/2026
1.0.2 119 6/12/2026
1.0.1 119 6/12/2026
1.0.0 115 6/12/2026