Compare commits

..
10 Commits
Author SHA1 Message Date
marco 3af03203c1 added architecture overview 2026-08-31 16:35:07 +02:00
marco e834ff623b added api design 2026-08-31 15:47:33 +02:00
marco db88d1d9d9 added rabbitmq docker step 2026-08-31 15:30:39 +02:00
marco 910cbadcc8 Added RabbitMQ references 2026-08-31 15:27:57 +02:00
marco bc38afcbf8 implemented rabbitmq 2026-08-31 15:27:07 +02:00
marco 7384cfcc84 updated references 2026-08-31 14:14:02 +02:00
marco 5fa35536cc fixed update function 2026-08-31 14:13:22 +02:00
marco ec91488f9a refactored error handling 2026-08-31 13:54:35 +02:00
marco de94725885 changed to lowercase string type 2026-08-31 13:49:57 +02:00
marco 5b64b32721 added exception handling 2026-08-31 13:43:43 +02:00
12 changed files with 171 additions and 30 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Do not edit this file with editors other than draw.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" style="background: #ffffff; background-color: var(--ge-adaptive-bg, #ffffff); color-scheme: light dark;" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="977px" height="272px" viewBox="0 0 977 272" id="ge-svg-el5gS3-sLhmpqUjrf2a7" content="&lt;mxfile host=&quot;app.diagrams.net&quot; scale=&quot;1&quot; border=&quot;0&quot;&gt;&#xA; &lt;diagram name=&quot;Page-1&quot; id=&quot;XI1LN9cxOzOCjr9QH5Ly&quot;&gt;&#xA; &lt;mxGraphModel dx=&quot;730&quot; dy=&quot;1198&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; tooltips=&quot;1&quot; connect=&quot;1&quot; arrows=&quot;1&quot; fold=&quot;1&quot; page=&quot;1&quot; pageScale=&quot;1&quot; pageWidth=&quot;850&quot; pageHeight=&quot;1100&quot; math=&quot;0&quot; shadow=&quot;0&quot;&gt;&#xA; &lt;root&gt;&#xA; &lt;mxCell id=&quot;0&quot; /&gt;&#xA; &lt;mxCell id=&quot;1&quot; parent=&quot;0&quot; /&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-1&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;&quot; value=&quot;Client&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;60&quot; width=&quot;120&quot; x=&quot;305&quot; y=&quot;670&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-2&quot; parent=&quot;1&quot; style=&quot;rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;&quot; value=&quot;&amp;lt;h3&amp;gt;API&amp;lt;/h3&amp;gt;&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;190&quot; width=&quot;570&quot; x=&quot;505&quot; y=&quot;600&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-8&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;uj0MX8yyicVLs1X-wsOb-3&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; target=&quot;uj0MX8yyicVLs1X-wsOb-4&quot; value=&quot;&quot;&gt;&#xA; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-11&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;uj0MX8yyicVLs1X-wsOb-3&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; target=&quot;uj0MX8yyicVLs1X-wsOb-10&quot; value=&quot;Nach erfolgreichem Speichern&quot;&gt;&#xA; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-3&quot; parent=&quot;1&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; value=&quot;Controller&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;60&quot; width=&quot;120&quot; x=&quot;550&quot; y=&quot;670&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-9&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;uj0MX8yyicVLs1X-wsOb-4&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;&quot; target=&quot;uj0MX8yyicVLs1X-wsOb-5&quot; value=&quot;&quot;&gt;&#xA; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-4&quot; parent=&quot;1&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; value=&quot;Service&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;60&quot; width=&quot;120&quot; x=&quot;730&quot; y=&quot;670&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-5&quot; parent=&quot;1&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; value=&quot;Repository&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;60&quot; width=&quot;120&quot; x=&quot;910&quot; y=&quot;670&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-6&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;uj0MX8yyicVLs1X-wsOb-1&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;innerLoopWaypoints=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.572;entryY=0.019;entryDx=0;entryDy=0;entryPerimeter=0;&quot; target=&quot;uj0MX8yyicVLs1X-wsOb-1&quot;&gt;&#xA; &lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-10&quot; parent=&quot;1&quot; style=&quot;rounded=0;whiteSpace=wrap;html=1;&quot; value=&quot;RabbitQM&quot; vertex=&quot;1&quot;&gt;&#xA; &lt;mxGeometry height=&quot;60&quot; width=&quot;120&quot; x=&quot;550&quot; y=&quot;810&quot; as=&quot;geometry&quot; /&gt;&#xA; &lt;/mxCell&gt;&#xA; &lt;mxCell id=&quot;uj0MX8yyicVLs1X-wsOb-12&quot; parent=&quot;1Line truncated

After

Width:  |  Height:  |  Size: 67 KiB

+68
View File
@@ -0,0 +1,68 @@
# Endpunkte
GET /api/person -> Get all persons
GET /api/person/username/{username} -> Get person
POST /api/person -> Create
PUT /api/person/username/{username} -> Update
DELETE /api/person/username/{username} -> Delete
# Personen-Modell
String username
String realName
DateOnly birthday
# Beispiele
## Beispiel 1
Request:
POST /api/person
{
"username": "marco",
"realName": "Marco",
"birthday": "2000-01-26"
}
Response: 201
## Beispiel 2
Request:
POST /api/person
{
"username": "m",
"realName": "Marco",
"birthday": "2000-01-26"
}
Response: 400
## Beispel 3
1. Request:
POST /api/person
{
"username": "marco",
"realName": "Marco",
"birthday": "2000-01-26"
}
Response: 201
2. Request:
POST /api/person
{
"username": "marco",
"realName": "Marco",
"birthday": "2000-01-26"
}
Response: 409
## Beispiel 4
Request:
POST /api/person
{
"username": "marco",
"realName": "Marco",
"birthday": "2020-01-26"
}
Response: 400 Person muss 16 Jahre alt sein
+17 -7
View File
@@ -1,19 +1,17 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using api.Services; using api.Services;
using api.DTOs; using api.DTOs;
using Microsoft.EntityFrameworkCore;
using api.RabbitMQ;
namespace api.Controllers namespace api.Controllers
{ {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class PersonController : ControllerBase public class PersonController(IPersonService personService, IMessageProducer messagePublisher) : ControllerBase
{ {
private readonly IPersonService _personService; private readonly IPersonService _personService = personService;
private readonly IMessageProducer _messagePublisher = messagePublisher;
public PersonController(IPersonService personService)
{
_personService = personService;
}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAll() public async Task<IActionResult> GetAll()
@@ -38,10 +36,22 @@ namespace api.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> Add(PersonDto personDto) public async Task<IActionResult> Add(PersonDto personDto)
{
try
{ {
await _personService.AddPersonAsync(personDto); await _personService.AddPersonAsync(personDto);
_messagePublisher.SendMessage<PersonDto>(personDto);
return CreatedAtAction(nameof(GetByUsername), new { username = personDto.Username }, personDto); return CreatedAtAction(nameof(GetByUsername), new { username = personDto.Username }, personDto);
} }
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (DbUpdateException)
{
return Conflict("Username already exists");
}
}
[HttpPut("username/{username}")] [HttpPut("username/{username}")]
public async Task<IActionResult> Update(string username, PersonDto personDto) public async Task<IActionResult> Update(string username, PersonDto personDto)
+15 -4
View File
@@ -24,14 +24,25 @@ public sealed class Person
realName = realName.Trim(); realName = realName.Trim();
// TODO: Error Handling // TODO: Error Handling
if (username.Length is < 3 or > 32) throw new Exception(); if (username.Length is < 3 or > 32) throw new ArgumentException("Username must be 3-32 chars.");
if (realName.Length == 0) throw new Exception(); if (realName.Length == 0) throw new ArgumentException("Name cannot be empty");
if (birthday >= DateOnly.FromDateTime(DateTime.Today)) throw new Exception(); if (!IsAtLeast16(birthday) || birthday >= DateOnly.FromDateTime(DateTime.Today)) throw new ArgumentException("Person must be at least 16 years old.");
if (!IsAtLeast16(birthday)) throw new Exception();
return new Person(username, realName, birthday); return new Person(username, realName, birthday);
} }
public void Update(string realName, DateOnly birthday)
{
realName = realName.Trim();
if (realName.Length == 0) throw new ArgumentException("Name cannot be empty");
if (!IsAtLeast16(birthday) || birthday >= DateOnly.FromDateTime(DateTime.Today))
throw new ArgumentException("Person must be at least 16 years old.");
RealName = realName;
Birthday = birthday;
}
// Konstruktor zum Erzeugen von Nutzerobjekten aus der Datenbank // Konstruktor zum Erzeugen von Nutzerobjekten aus der Datenbank
internal static Person FromStorage(string username, string realName, DateOnly birthday) internal static Person FromStorage(string username, string realName, DateOnly birthday)
=> new(username, realName, birthday); => new(username, realName, birthday);
+4
View File
@@ -2,6 +2,8 @@ using Microsoft.EntityFrameworkCore;
using api.Data; using api.Data;
using api.Repositories; using api.Repositories;
using api.Services; using api.Services;
using RabbitMQ.Client;
using api.RabbitMQ;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -17,6 +19,8 @@ builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(builder
builder.Services.AddScoped<IPersonRepository, PersonRepository>(); builder.Services.AddScoped<IPersonRepository, PersonRepository>();
builder.Services.AddScoped<IPersonService, PersonService>(); builder.Services.AddScoped<IPersonService, PersonService>();
builder.Services.AddSingleton<IMessageProducer, RabbitMQProducer>();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
+7
View File
@@ -0,0 +1,7 @@
namespace api.RabbitMQ
{
public interface IMessageProducer
{
void SendMessage<T> (T message);
}
}
+32
View File
@@ -0,0 +1,32 @@
using api.DTOs;
using RabbitMQ.Client;
using System.Text;
using System.Text.Json;
namespace api.RabbitMQ;
public class RabbitMQProducer : IMessageProducer
{
private IChannel? channel;
private readonly string personQueueName = "persons";
public async void SendMessage<T>(T message)
{
if (channel == null)
{
ConnectionFactory factory = new ConnectionFactory
{
Uri = new Uri("amqp://guest:guest@localhost:5672")
};
IConnection conn = await factory.CreateConnectionAsync();
channel = await conn.CreateChannelAsync();
await channel.QueueDeclareAsync(personQueueName);
}
if (message is PersonDto)
{
string jsonString = JsonSerializer.Serialize(message);
byte[] body = Encoding.UTF8.GetBytes(jsonString);
_ = channel.BasicPublishAsync(exchange: "", routingKey: personQueueName, body: body);
}
}
}
+2 -2
View File
@@ -6,9 +6,9 @@ namespace api.Repositories
public interface IPersonRepository public interface IPersonRepository
{ {
Task<IEnumerable<Person>> GetAllAsync(); Task<IEnumerable<Person>> GetAllAsync();
Task<Person> GetByUsernameAsync(String username); Task<Person> GetByUsernameAsync(string username);
Task AddAsync(Person person); Task AddAsync(Person person);
Task UpdateAsync(Person person); Task UpdateAsync(Person person);
Task DeleteAsync(String username); Task DeleteAsync(string username);
} }
} }
+3 -5
View File
@@ -20,7 +20,7 @@ public class PersonRepository(AppDbContext context) : IPersonRepository
public async Task<Person> GetByUsernameAsync(string username) public async Task<Person> GetByUsernameAsync(string username)
{ {
return await _context.Persons.FindAsync(username); return await _context.Persons.FindAsync(username) ?? throw new KeyNotFoundException("User does not exist");
} }
public async Task UpdateAsync(Person person) public async Task UpdateAsync(Person person)
{ {
@@ -30,14 +30,12 @@ public class PersonRepository(AppDbContext context) : IPersonRepository
public async Task DeleteAsync(string username) public async Task DeleteAsync(string username)
{ {
Person person = await _context.Persons.FindAsync(username); Person person = await _context.Persons.FindAsync(username) ?? throw new KeyNotFoundException("User does not exist");;
if (person != null)
{
_context.Persons.Remove(person); _context.Persons.Remove(person);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
}
} }
+8 -9
View File
@@ -33,7 +33,7 @@ namespace api.Services
public async Task<PersonDto> GetPersonByUsernameAsync(string username) public async Task<PersonDto> GetPersonByUsernameAsync(string username)
{ {
Person person = await personExists(username); Person person = await PersonExists(username);
return new PersonDto return new PersonDto
{ {
@@ -45,27 +45,26 @@ namespace api.Services
public async Task UpdatePersonAsync(string username, PersonDto personDto) public async Task UpdatePersonAsync(string username, PersonDto personDto)
{ {
Person person = await PersonExists(username);
await personExists(username); string updatedName = string.IsNullOrWhiteSpace(personDto.RealName) ? person.RealName : personDto.RealName;
Person updatedPerson = Person.Create(username: username, realName: personDto.RealName, birthday: personDto.Birthday); person.Update(realName: updatedName, birthday: personDto.Birthday);
await _personRepository.UpdateAsync(updatedPerson); await _personRepository.UpdateAsync(person);
} }
public async Task DeletePersonAsync(string username) public async Task DeletePersonAsync(string username)
{ {
await personExists(username); await PersonExists(username);
await _personRepository.DeleteAsync(username); await _personRepository.DeleteAsync(username);
} }
private async Task<Person> personExists(string username) private async Task<Person> PersonExists(string username)
{ {
username = Person.NormalizeUsername(username); username = Person.NormalizeUsername(username);
Person? person = await _personRepository.GetByUsernameAsync(username); Person person = await _personRepository.GetByUsernameAsync(username);
if (person == null) throw new KeyNotFoundException($"Person '{username}' not found");
return person; return person;
} }
+1
View File
@@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
</ItemGroup> </ItemGroup>
+10 -3
View File
@@ -4,10 +4,17 @@ Genutze Ressourcen zum Recherchieren
2. [ASP.NET mit postgres tutorial](https://dev.to/olymahmud/building-a-crud-api-with-aspnet-core-web-api-and-postgresql-p5f) 2. [ASP.NET mit postgres tutorial](https://dev.to/olymahmud/building-a-crud-api-with-aspnet-core-web-api-and-postgresql-p5f)
3. [Change tracking](https://learn.microsoft.com/en-us/ef/core/change-tracking/)
4. [RabbitMQ Doku](https://www.rabbitmq.com/client-libraries/dotnet-api-guide#connecting)
5. [RabbitMQ Tutorial](https://code-maze.com/aspnetcore-rabbitmq/)
# Projekt starten (Linux/Docker) # Projekt starten (Linux/Docker)
1. `docker run --name postgres -e POSTGRES_PASSWORD=postgres --network host -d postgres` 1. `docker run --name postgres -e POSTGRES_PASSWORD=postgres --network host -d postgres`
2. `psql -U postgres -d postgres -h localhost -p 5432 -f db/schema.sql` 2. `psql -U postgres -d postgres -h localhost -p 5432 -f db/schema.sql`
3. `cd api` 3. `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4-management`
4. `dotnet run` 4. `cd api`
5. http://localhost:5007/swagger im Browser aufrufen 5. `dotnet run`
6. http://localhost:5007/swagger im Browser aufrufen