Практикум WPF — сервер ASP.NET Core Web API
Практикум, шаг 3 из 6. Поднимаем TaskDesk.Api — REST-сервер для десктоп-клиента. Базовый цикл ASP.NET — 4511; Minimal API — 4517; REST — 1151.
Контракт API
Ресурс — задача (Task). Базовый префикс /api/v1/tasks.
| Метод | URL | Тело | Ответ |
|---|---|---|---|
GET | /api/v1/tasks | — | 200 массив TaskDto |
GET | /api/v1/tasks/{id} | — | 200 или 404 |
POST | /api/v1/tasks | { "title", "status" } | 201 + Location |
PUT | /api/v1/tasks/{id} | полный TaskDto | 200 или 404 |
DELETE | /api/v1/tasks/{id} | — | 204 или 404 |
Фильтр (опционально): GET /api/v1/tasks?status=Todo.
Структура проекта
mkdir TaskDesk
cd TaskDesk
dotnet new sln -n TaskDesk
dotnet new webapi -n TaskDesk.Api -o src/TaskDesk.Api --use-controllers
dotnet new classlib -n TaskDesk.Core -o src/TaskDesk.Core
dotnet sln add src/TaskDesk.Api src/TaskDesk.Core
dotnet add src/TaskDesk.Api reference src/TaskDesk.Core
TaskDesk/
├── TaskDesk.sln
└── src/
├── TaskDesk.Api/
│ ├── Program.cs
│ ├── Controllers/TasksController.cs
│ └── Services/InMemoryTaskStore.cs
└── TaskDesk.Core/
├── Models/TaskItem.cs
└── Contracts/TaskDto.cs
DTO — контракт JSON
namespace TaskDesk.Core.Contracts;
public record TaskDto(
Guid Id,
string Title,
string Status,
DateTimeOffset CreatedAt);
public record CreateTaskRequest(string Title, string Status = "Todo");
public record UpdateTaskRequest(string Title, string Status);
DTO отделены от доменной модели — клиент WPF и тесты завязаны на стабильный JSON, а не на внутренние классы сервера.
Хранилище и DI
public interface ITaskStore
{
IReadOnlyList<TaskItem> GetAll(string? statusFilter);
TaskItem? GetById(Guid id);
TaskItem Add(TaskItem item);
bool Update(TaskItem item);
bool Delete(Guid id);
}
public sealed class InMemoryTaskStore : ITaskStore
{
private readonly List<TaskItem> _items = new();
private readonly object _lock = new();
public IReadOnlyList<TaskItem> GetAll(string? statusFilter)
{
lock (_lock)
{
var q = _items.AsEnumerable();
if (!string.IsNullOrEmpty(statusFilter))
q = q.Where(t => t.Status.ToString() == statusFilter);
return q.OrderByDescending(t => t.CreatedAt).ToList();
}
}
// Add, Update, Delete — с lock и Guid
}
Регистрация в Program.cs:
builder.Services.AddSingleton<ITaskStore, InMemoryTaskStore>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Для учебного проекта достаточно памяти; прод-вариант — EF Core + SQLite (453).
Контроллер REST
[ApiController]
[Route("api/v1/[controller]")]
public class TasksController : ControllerBase
{
private readonly ITaskStore _store;
public TasksController(ITaskStore store) => _store = store;
[HttpGet]
public ActionResult<IEnumerable<TaskDto>> GetAll([FromQuery] string? status)
{
var items = _store.GetAll(status).Select(Map);
return Ok(items);
}
[HttpGet("{id:guid}")]
public ActionResult<TaskDto> GetById(Guid id)
{
var item = _store.GetById(id);
return item is null ? NotFound() : Ok(Map(item));
}
[HttpPost]
public ActionResult<TaskDto> Create([FromBody] CreateTaskRequest request)
{
if (string.IsNullOrWhiteSpace(request.Title))
return BadRequest(new { error = "Title is required" });
var entity = new TaskItem
{
Title = request.Title.Trim(),
Status = Enum.Parse<TaskStatus>(request.Status, ignoreCase: true)
};
_store.Add(entity);
var dto = Map(entity);
return CreatedAtAction(nameof(GetById), new { id = dto.Id }, dto);
}
[HttpPut("{id:guid}")]
public ActionResult<TaskDto> Update(Guid id, [FromBody] UpdateTaskRequest request)
{
var existing = _store.GetById(id);
if (existing is null) return NotFound();
existing.Title = request.Title.Trim();
existing.Status = Enum.Parse<TaskStatus>(request.Status, ignoreCase: true);
_store.Update(existing);
return Ok(Map(existing));
}
[HttpDelete("{id:guid}")]
public IActionResult Delete(Guid id) =>
_store.Delete(id) ? NoContent() : NotFound();
private static TaskDto Map(TaskItem t) =>
new(t.Id, t.Title, t.Status.ToString(), t.CreatedAt);
}
Program.cs — pipeline, Swagger, CORS
Десктоп-клиент на другом origin (или localhost с другим портом) требует CORS:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("TaskDeskClient", policy =>
policy.WithOrigins("http://localhost:5000") // dev WPF через proxy или *
.AllowAnyHeader()
.AllowAnyMethod());
});
// ... AddControllers, Swagger, ITaskStore
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors("TaskDeskClient");
app.MapControllers();
app.Run();
WPF-приложение не является браузером — HttpClient не применяет CORS так же, как Chrome. Политика CORS нужна, если клиент ходит через WebView или вы тестируете из Swagger/браузера. Для чистого HttpClient из WPF достаточно корректного URL и TLS; CORS оставляем для единообразия с веб-инструментами и будущим Blazor Hybrid.
Запуск:
cd src/TaskDesk.Api
dotnet run --urls http://localhost:5100
Swagger UI: http://localhost:5100/swagger.
Проверка вручную
curl -X GET http://localhost:5100/api/v1/tasks
curl -X POST http://localhost:5100/api/v1/tasks `
-H "Content-Type: application/json" `
-d "{\"title\":\"Настроить Prism\"}"
Ожидаем 201 и JSON с id.
Ошибки и коды
| Код | Когда |
|---|---|
400 | Пустой заголовок, неверный enum статуса |
404 | Нет задачи с таким id |
500 | Необработанное исключение — логируем, клиенту краткий JSON |
Единый формат ошибки упрощает отображение в WPF (ErrorMessage в ViewModel).
Чек-лист шага 3
- API отвечает на
GETиPOST/api/v1/tasks. - Swagger открывается в Development.
- DTO в
TaskDesk.Coreсовпадают с тем, что ожидает клиент. - Сервис зарегистрирован через DI.
Дальше: Клиент WPF на Prism.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). WPF как презентационный слой .NET — дерево XAML, layout, привязки, ресурсы и связь с практикумом TaskDesk. Model, View, ViewModel, INotifyPropertyChanged, ICommand, CommunityToolkit.Mvvm и тестируемая логика для TaskDesk. Prism для WPF — модули, регионы, DI, INavigationService, HttpClient и ApiTaskRepository для TaskDesk.Client. Postman и Swagger для REST TaskDesk, WebApplicationFactory, xUnit, Moq для ViewModel и репозитория. Полноценное клиент-серверное приложение — solution, сборка, сценарии демо, расширения и чек-лист готовности.Практикум WPF — введение в WPF и XAML
Практикум WPF — основы MVVM
Практикум WPF — клиент на Prism
Практикум WPF — тестирование API и unit-тесты
Практикум WPF — итоговый проект TaskDesk