WebApi + ClientApp, GraphQL, Reflection

This commit is contained in:
Boris Milašinović
2026-05-06 20:55:05 +02:00
parent 8f7c704a90
commit 4fb3de19f6
196 changed files with 10395 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
using Events.WebAPI.Contract.Command;
using Events.WebAPI.Contract.DTOs;
using Events.WebAPI.Contract.Queries.Generic;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using MobilityOne.Common.Commands;
namespace Events.WebAPI.Controllers.Generic;
public abstract class CrudController<TDto, TPK> : GetController<TDto, TPK>
where TDto : class, IHasIdAsPK<TPK>
where TPK : IEquatable<TPK>
{
/// <summary>
/// Creates a new item.
/// </summary>
/// <param name="model">id does not have to be sent (if sent it would be ignored)</param>
/// <param name="mediator"></param>
/// <returns>A newly created item</returns>
/// <response code="201">Returns the newly created item (route to the item, and the item in the body)</response>
/// <response code="400">If the model is null or not valid</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = nameof(Policies.EditData))]
public virtual async Task<ActionResult<TDto>> Create(TDto model, [FromServices] IMediator mediator)
{
//Note: It never produce ActionResult<TDto> but we need this because of Swagger description
//We cannot user generic type in attributes (i.e. in ProducesResponseType)
//if successful it returns ActionResult with Value:null and Result:CreatedAtAction
//Thus result.Result.Value is TDto
var command = new AddCommand<TDto, TPK>(model);
TPK id = await mediator.Send(command);
var query = new GetSingleItemQuery<TDto, TPK>(id);
var item = await mediator.Send(query);
var action = CreatedAtAction(nameof(Get), new { id }, item);
return action;
}
/// <summary>
/// Update the item
/// </summary>
/// <param name="id"></param>
/// <param name="model"></param>
/// <param name="mediator"></param>
/// <returns></returns>
/// <response code="204">if the update was successful</response>
/// <response code="404">if there is no item with sent id, or if a user does not have a permission to update the item</response>
/// <response code="400">If the model is not valid</response>
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = nameof(Policies.EditData))]
public virtual async Task<IActionResult> Update(TPK id, TDto model, [FromServices] IMediator mediator)
{
if (!model.Id.Equals(id)) //ModelState.IsValid & model != null checked automatically due to [ApiController]
{
return Problem(statusCode: StatusCodes.Status400BadRequest, detail: $"Different ids: {id} vs {model.Id}");
}
else
{
var query = new GetSingleItemQuery<TDto, TPK>(id);
var item = await mediator.Send(query);
if (item == null)
{
return Problem(statusCode: StatusCodes.Status404NotFound, detail: $"Invalid id = {id}");
}
await DoUpdate(model, mediator);
return NoContent();
}
}
private static async Task DoUpdate(TDto model, IMediator mediator)
{
var command = new UpdateCommand<TDto>(model);
await mediator.Send(command);
}
/// <summary>
/// Partially update the item
/// </summary>
/// <param name="id"></param>
/// <param name="delta">RFC 6902 formatted json</param>
/// <param name="mediator"></param>
/// <returns></returns>
/// <response code="204">if the update was successful</response>
/// <response code="404">if there is no item with sent id, or if a user does not have a permission to update the item</response>
/// <response code="400">If the patched model is not valid</response>
[HttpPatch("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = nameof(Policies.EditData))]
public virtual async Task<IActionResult> UpdatePartially(TPK id,
JsonPatchDocument<TDto> delta,
[FromServices] IMediator mediator)
{
//Get current DTO based on id (Get will also check for permission to run patch in contrast to direct retrieval)(
var getResult = await base.Get(id, mediator);
if (getResult.Value != null)
{
string problem = string.Empty;
bool ok = true;
TDto dto = getResult.Value;
delta.ApplyTo(dto, patchError =>
{
ok = false;
problem = $"{patchError.Operation} causing error: {patchError.ErrorMessage}";
});
if (ok)
{
if (!dto.Id.Equals(id)) //ensures that id has not been changed
{
problem = $"Id mismatch after patching {id} <> {dto.Id}";
return Problem(detail: problem, statusCode: StatusCodes.Status400BadRequest);
}
else
{
await DoUpdate(dto, mediator);
return NoContent();
}
}
else
{
return Problem(detail: problem, statusCode: StatusCodes.Status400BadRequest);
}
}
else
{
return getResult.Result;
}
}
/// <summary>
/// Delete the item base on primary key value (id)
/// </summary>
/// <param name="id">Primary key value</param>
/// <param name="mediator">Query/Command (Request) mediator. (Obtained using Dependency Injection from services)</param>
/// <returns></returns>
/// <response code="204">If the item is deleted</response>
/// <response code="404">If the item with id does not exist</response>
/// <response code="400">If the valiation exists, and fails</response>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Authorize(Policy = nameof(Policies.EditData))]
public virtual async Task<IActionResult> Delete(TPK id, [FromServices] IMediator mediator)
{
var query = new GetSingleItemQuery<TDto, TPK>(id);
var item = await mediator.Send(query);
if (item == null)
{
return Problem(statusCode: StatusCodes.Status404NotFound, detail: $"Invalid id = {id}");
}
var command = new DeleteCommand<TDto, TPK>(id);
await mediator.Send(command);
return NoContent();
}
}

View File

@@ -0,0 +1,89 @@
using AutoMapper;
using Events.WebAPI.Contract.DTOs;
using Events.WebAPI.Contract.Queries.Generic;
using Events.WebAPI.Models;
using Events.WebAPI.Util.Middleware;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Events.WebAPI.Controllers.Generic;
[Authorize(Policy = nameof(Policies.ReadData))]
[ApiController]
[Route("[controller]")]
[TypeFilter(typeof(BadRequestOnRuleValidationException), Order = 20)]
[TypeFilter(typeof(ProblemDetailsForSqlException), Order = 10)]
[TypeFilter(typeof(ProblemDetailsForException), Order = 1)] //last one
public abstract class GetController<TDto, TPK> : ControllerBase
where TPK : IEquatable<TPK>
{
/// <summary>
/// Get number of item satisfying filters
/// </summary>
/// <param name="filters">Each filter is like key(operator)value, using Sieve syntax</param>
/// <param name="mediator"></param>
/// <param name="mapper"></param>
/// <returns></returns>
[HttpGet(nameof(Count))]
public virtual async Task<int> Count(string filters, [FromServices] IMediator mediator, [FromServices] IMapper mapper)
{
var countRequest = new GetCountQuery<TDto>
{
Filters = filters,
};
int count = await mediator.Send(countRequest);
return count;
}
/// <summary>
/// Returns single item based on primary key value
/// </summary>
/// <param name="id"></param>
/// <param name="mediator"></param>
/// <returns></returns>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public virtual async Task<ActionResult<TDto>> Get(TPK id, [FromServices] IMediator mediator)
{
var query = new GetSingleItemQuery<TDto, TPK>(id);
var item = await mediator.Send(query);
return item != null ? item : Problem(statusCode: StatusCodes.Status404NotFound, detail: $"No data for id = {id}");
}
/// <summary>
/// Get all items based on (lazy) load parameters (paging, sorting, and filtering)
/// </summary>
/// <param name="loadParams"></param>
/// <param name="mediator"></param>
/// <param name="mapper"></param>
/// <returns></returns>
[HttpGet]
public virtual async Task<Items<TDto>> GetAll([FromQuery] LoadParams loadParams, [FromServices] IMediator mediator, [FromServices] IMapper mapper)
{
loadParams ??= new();
var result = new Items<TDto>();
var countRequest = new GetCountQuery<TDto>
{
Filters = loadParams.Filters
};
result.Count = await mediator.Send(countRequest);
if (result.Count > 0)
{
var dataRequest = new GetItemsQuery<TDto>
{
Filters = loadParams.Filters,
Sort = loadParams.Sort,
Page = loadParams.Page,
PageSize = loadParams.PageSize,
Ascending = loadParams.Ascending
};
result.Data = await mediator.Send(dataRequest);
}
return result;
}
}