Solution Explorer
📁 th3chris.API
📄 Program.cs
📄 UsersController.cs
📄 Startup.cs
📁 Controllers
📄 AuthController.cs
📄 ProjectsController.cs
📄 OrdersController.cs
📁 Models
📄 User.cs
📄 Project.cs
📄 Order.cs
📄 BaseEntity.cs
📁 Services
📄 UserService.cs
📄 IUserService.cs
📄 AuthService.cs
📄 EmailService.cs
📁 Data
📄 AppDbContext.cs
📄 DbInitializer.cs
📁 DTOs
📄 UserDto.cs
📄 CreateUserRequest.cs
📄 UpdateUserRequest.cs
📁 Middleware
📄 ErrorHandlingMiddleware.cs
📄 LoggingMiddleware.cs
📁 Extensions
📄 ServiceExtensions.cs
📄 ControllerExtensions.cs
UsersController.cs
// File: UsersController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
using th3chris.API.Models;
using th3chris.API.Services;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using AutoMapper;
using FluentValidation;
namespace th3chris.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
private readonly IMapper _mapper;
private readonly IValidator<CreateUserRequest> _createValidator;
private readonly IValidator<UpdateUserRequest> _updateValidator;
public UsersController(
IUserService userService,
ILogger<UsersController> logger,
IMapper mapper,
IValidator<CreateUserRequest> createValidator,
IValidator<UpdateUserRequest> updateValidator)
{
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_createValidator = createValidator;
_updateValidator = updateValidator;
}
/// <summary>
/// Retrieves all users with pagination support
/// </summary>
/// <param name="pageNumber">Page number (default: 1)</param>
/// <param name="pageSize">Page size (default: 10, max: 100)</param>
/// <returns>Paginated list of users</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<PagedResult<UserDto>>> GetUsers(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? search = null,
[FromQuery] string? sortBy = null,
[FromQuery] bool sortDescending = false)
{
try
{
if (pageNumber < 1)
return BadRequest("Page number must be greater than 0");
if (pageSize < 1 || pageSize > 100)
return BadRequest("Page size must be between 1 and 100");
_logger.LogInformation("Getting users - Page: {PageNumber}, Size: {PageSize}, Search: {Search}",
pageNumber, pageSize, search);
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var filter = new UserFilter
{
PageNumber = pageNumber,
PageSize = pageSize,
Search = search,
SortBy = sortBy,
SortDescending = sortDescending,
RequestingUserId = currentUserId
};
var users = await _userService.GetUsersAsync(filter);
var userDtos = _mapper.Map<IEnumerable<UserDto>>(users.Items);
var result = new PagedResult<UserDto>
{
Items = userDtos,
TotalCount = users.TotalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling((double)users.TotalCount / pageSize)
};
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting users");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Retrieves a specific user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>User details</returns>
[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDetailDto>> GetUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Getting user {UserId} requested by {CurrentUserId}", id, currentUserId);
var user = await _userService.GetUserByIdAsync(id, currentUserId);
if (user == null)
{
_logger.LogWarning("User {UserId} not found or access denied for {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
var userDto = _mapper.Map<UserDetailDto>(user);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized access attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while getting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Creates a new user
/// </summary>
/// <param name="request">User creation request</param>
/// <returns>Created user</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<UserDto>> CreateUser([FromBody] CreateUserRequest request)
{
try
{
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _createValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Creating new user {Email} by {CurrentUserId}", request.Email, currentUserId);
var user = _mapper.Map<User>(request);
user.CreatedBy = currentUserId;
user.CreatedAt = DateTime.UtcNow;
var createdUser = await _userService.CreateUserAsync(user);
var userDto = _mapper.Map<UserDto>(createdUser);
return CreatedAtAction(nameof(GetUser), new { id = createdUser.Id }, userDto);
}
catch (DuplicateUserException ex)
{
_logger.LogWarning(ex, "Attempt to create duplicate user {Email}", request?.Email);
return Conflict(new { Message = "User with this email already exists" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while creating user");
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Updates an existing user
/// </summary>
/// <param name="id">User ID</param>
/// <param name="request">User update request</param>
/// <returns>Updated user</returns>
[HttpPut("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
if (request == null)
return BadRequest("Request body is required");
var validationResult = await _updateValidator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors.Select(e => e.ErrorMessage);
return BadRequest(new { Errors = errors });
}
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Updating user {UserId} by {CurrentUserId}", id, currentUserId);
var existingUser = await _userService.GetUserByIdAsync(id, currentUserId);
if (existingUser == null)
{
_logger.LogWarning("User {UserId} not found for update by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_mapper.Map(request, existingUser);
existingUser.UpdatedBy = currentUserId;
existingUser.UpdatedAt = DateTime.UtcNow;
var updatedUser = await _userService.UpdateUserAsync(existingUser);
var userDto = _mapper.Map<UserDto>(updatedUser);
return Ok(userDto);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized update attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while updating user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
/// <summary>
/// Deletes a user by ID
/// </summary>
/// <param name="id">User ID</param>
/// <returns>No content on success</returns>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IActionResult> DeleteUser(int id)
{
try
{
if (id <= 0)
return BadRequest("Invalid user ID");
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
_logger.LogInformation("Deleting user {UserId} by {CurrentUserId}", id, currentUserId);
var success = await _userService.DeleteUserAsync(id, currentUserId);
if (!success)
{
_logger.LogWarning("User {UserId} not found for deletion by {CurrentUserId}", id, currentUserId);
return NotFound($"User with ID {id} not found");
}
_logger.LogInformation("User {UserId} successfully deleted by {CurrentUserId}", id, currentUserId);
return NoContent();
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unauthorized deletion attempt for user {UserId}", id);
return Forbid();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while deleting user {UserId}", id);
return StatusCode(StatusCodes.Status500InternalServerError,
"An error occurred while processing your request");
}
}
}
}
// File: UserService.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using th3chris.API.Models;
using th3chris.API.Data;
using th3chris.API.Exceptions;
namespace th3chris.API.Services
{
public class UserService : IUserService
{
private readonly AppDbContext _context;
private readonly ILogger<UserService> _logger;
public UserService(AppDbContext context, ILogger<UserService> logger)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<PagedResult<User>> GetUsersAsync(UserFilter filter)
{
var query = _context.Users.AsQueryable();
if (!string.IsNullOrWhiteSpace(filter.Search))
{
var searchTerm = filter.Search.ToLower();
query = query.Where(u => u.Email.ToLower().Contains(searchTerm) ||
u.FirstName.ToLower().Contains(searchTerm) ||
u.LastName.ToLower().Contains(searchTerm));
}
var totalCount = await query.CountAsync();
if (!string.IsNullOrWhiteSpace(filter.SortBy))
{
query = ApplySorting(query, filter.SortBy, filter.SortDescending);
}
else
{
query = query.OrderBy(u => u.LastName).ThenBy(u => u.FirstName);
}
var users = await query
.Skip((filter.PageNumber - 1) * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();
return new PagedResult<User>
{
Items = users,
TotalCount = totalCount
};
}
}
}
Test Explorer
GetUsers_ReturnsOk
GetUsers_WithPagination
GetUsers_WithSearch
GetUser_ValidId_ReturnsUser
GetUser_InvalidId_NotFound
CreateUser_ValidData
CreateUser_DuplicateEmail
CreateUser_InvalidData
UpdateUser_ValidData
UpdateUser_NotFound
UpdateUser_Unauthorized
DeleteUser_Success
DeleteUser_NotFound
DeleteUser_Unauthorized
Auth_Login_ValidCredentials
Auth_Login_InvalidPassword
Auth_Register_NewUser
Email_SendWelcome_Success
Validation_User_RequiredFields
Validation_Email_Format
Zurück zum Blog
RAGKIDifyWeaviate

RAG-Systeme in der Praxis: Was funktioniert und was nicht

13. Februar 20262 Min. Lesezeit

## Warum RAG und nicht Fine-Tuning?

Retrieval-Augmented Generation (RAG) hat sich als pragmatischer Ansatz etabliert, um Large Language Models mit aktuellem, domänenspezifischem Wissen zu versorgen — ohne die Kosten und Komplexität von Fine-Tuning.

In der Praxis bedeutet das: Ihre bestehenden Dokumente, Wissensdatenbanken und FAQs werden in eine Vektordatenbank indexiert. Bei jeder Anfrage sucht das System die relevantesten Passagen und übergibt sie als Kontext an das LLM.

Die drei häufigsten Fehler

1. Zu große Chunks

Viele RAG-Implementierungen scheitern an zu großen Textblöcken. Ein Chunk von 2000 Tokens enthält zu viel irrelevante Information — das LLM muss die Nadel im Heuhaufen finden.

Besser: 300-500 Tokens pro Chunk, mit Überlappung von 50-100 Tokens. Die optimale Größe hängt vom Dokumenttyp ab.

2. Keine hybride Suche

Reine Vektorsuche hat blinde Flecken bei exakten Begriffen, Produktnummern oder Eigennamen. Eine Kombination aus Vektorsuche und Keyword-Suche (BM25) liefert deutlich bessere Ergebnisse.

3. Fehlende Evaluation

Ohne systematische Evaluation wissen Sie nicht, ob Ihr RAG-System tatsächlich bessere Antworten liefert. Erstellen Sie ein Testset mit Frage-Antwort-Paaren und messen Sie Precision, Recall und Answer Relevance.

Mein Stack

Für diese Website setze ich ein Self-Hosted RAG-System ein:

  • **Dify** als Orchestrierungsplattform (Open Source)
  • **Weaviate** als Vektordatenbank
  • **GPT-4** als LLM für die Antwortgenerierung
  • **Kubernetes** für das Hosting (k3s Cluster)
  • **ArgoCD** für GitOps-Deployments

Fazit

RAG ist kein Plug-and-Play. Die Qualität hängt von vielen Faktoren ab — Chunking, Embedding-Modell, Retrieval-Strategie und Prompt Engineering. Aber wenn es richtig gemacht wird, ist es der schnellste Weg, KI mit Ihrem Unternehmenswissen zu verbinden.