Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af03203c1 | ||
|
|
e834ff623b | ||
|
|
db88d1d9d9 | ||
|
|
910cbadcc8 | ||
|
|
bc38afcbf8 | ||
|
|
7384cfcc84 | ||
|
|
5fa35536cc | ||
|
|
ec91488f9a | ||
|
|
de94725885 | ||
|
|
5b64b32721 |
No files matched your search
@@ -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="<mxfile host="app.diagrams.net" scale="1" border="0">
 <diagram name="Page-1" id="XI1LN9cxOzOCjr9QH5Ly">
 <mxGraphModel dx="730" dy="1198" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
 <root>
 <mxCell id="0" />
 <mxCell id="1" parent="0" />
 <mxCell id="uj0MX8yyicVLs1X-wsOb-1" parent="1" style="rounded=1;whiteSpace=wrap;html=1;" value="Client" vertex="1">
 <mxGeometry height="60" width="120" x="305" y="670" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-2" parent="1" style="rounded=1;whiteSpace=wrap;html=1;verticalAlign=top;" value="&lt;h3&gt;API&lt;/h3&gt;" vertex="1">
 <mxGeometry height="190" width="570" x="505" y="600" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-8" edge="1" parent="1" source="uj0MX8yyicVLs1X-wsOb-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="uj0MX8yyicVLs1X-wsOb-4" value="">
 <mxGeometry relative="1" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-11" edge="1" parent="1" source="uj0MX8yyicVLs1X-wsOb-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="uj0MX8yyicVLs1X-wsOb-10" value="Nach erfolgreichem Speichern">
 <mxGeometry relative="1" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-3" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Controller" vertex="1">
 <mxGeometry height="60" width="120" x="550" y="670" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-9" edge="1" parent="1" source="uj0MX8yyicVLs1X-wsOb-4" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" target="uj0MX8yyicVLs1X-wsOb-5" value="">
 <mxGeometry relative="1" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-4" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Service" vertex="1">
 <mxGeometry height="60" width="120" x="730" y="670" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-5" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="Repository" vertex="1">
 <mxGeometry height="60" width="120" x="910" y="670" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-6" edge="1" parent="1" source="uj0MX8yyicVLs1X-wsOb-1" style="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;" target="uj0MX8yyicVLs1X-wsOb-1">
 <mxGeometry relative="1" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-10" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="RabbitQM" vertex="1">
 <mxGeometry height="60" width="120" x="550" y="810" as="geometry" />
 </mxCell>
 <mxCell id="uj0MX8yyicVLs1X-wsOb-12" parent="1Line truncated
|
||||||
|
After Width: | Height: | Size: 67 KiB |
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -39,8 +37,20 @@ namespace api.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Add(PersonDto personDto)
|
public async Task<IActionResult> Add(PersonDto personDto)
|
||||||
{
|
{
|
||||||
await _personService.AddPersonAsync(personDto);
|
try
|
||||||
return CreatedAtAction(nameof(GetByUsername), new { username = personDto.Username }, personDto);
|
{
|
||||||
|
await _personService.AddPersonAsync(personDto);
|
||||||
|
_messagePublisher.SendMessage<PersonDto>(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}")]
|
||||||
|
|||||||
+16
-5
@@ -24,18 +24,29 @@ 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);
|
||||||
|
|
||||||
public static string NormalizeUsername(string username)
|
public static string NormalizeUsername(string username)
|
||||||
=> username.Trim().ToLowerInvariant();
|
=> username.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace api.RabbitMQ
|
||||||
|
{
|
||||||
|
public interface IMessageProducer
|
||||||
|
{
|
||||||
|
void SendMessage<T> (T message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user