Skip to content

Adds a crud controller base #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="7.0.10" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Collections.Immutable;
using System.Text;
using System.Text.Json;
using Codehard.Common.AspNetCore.Controllers.Parameters;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;

namespace Codehard.Common.AspNetCore.Controllers;

[Route("api/[controller]")]
public abstract class CrudControllerBase<TEntityKey, TEntity> : ControllerBase
where TEntity : class
{
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken = default)
{
var requestParams =
this.Request.Query
.Select(q => new GetRequestParameter(q.Key, q.Value))
.ToImmutableArray();

var res = await this.ReadAsync(requestParams, cancellationToken);

return res;
}

[HttpPost]
public async Task<IActionResult> Post(CancellationToken cancellationToken = default)
{
var parameter = await GetParameter();

var res = await this.CreateAsync(parameter, cancellationToken);

return res;
}

[HttpPut]
public async Task<IActionResult> Put(CancellationToken cancellationToken = default)
{
var parameter = await GetParameter();

var res = await this.UpdateAsync(parameter, cancellationToken);

return res;
}

[HttpPatch]
public async Task<IActionResult> Patch(
[FromBody] JsonPatchDocument<TEntity>? patch,
CancellationToken cancellationToken = default)
{
var res = await this.PartialUpdateAsync(patch, cancellationToken);

return res;
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(
TEntityKey id,
CancellationToken cancellationToken = default)
{
var res = await this.DeleteAsync(id, cancellationToken);

return res;
}

private async Task<ControllerParameter> GetParameter()
{
var parameter =
(this.Request.ContentType switch
{
"application/json" => await ReadBodyAsync(this.Request),
_ when this.Request.HasFormContentType => await ReadFormDataAsync(this.Request),
_ => new ControllerParameter.Unknown(this.Request),
})!;

return parameter;

static async Task<ControllerParameter> ReadFormDataAsync(HttpRequest request)
{
var forms = await request.ReadFormAsync();

return new ControllerParameter.Form(forms);
}

static async Task<ControllerParameter> ReadBodyAsync(HttpRequest request)
{
using var sr = new StreamReader(request.Body, Encoding.UTF8);

var json = await sr.ReadToEndAsync();

return new ControllerParameter.Json(JsonDocument.Parse(json));
}
}

/// <summary>
/// Perform a read operation in an asynchronous manner.
/// </summary>
/// <param name="parameters"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract ValueTask<IActionResult> ReadAsync(
IReadOnlyCollection<GetRequestParameter> parameters,
CancellationToken cancellationToken);

/// <summary>
/// Perform a create operation in an asynchronous manner.
/// </summary>
/// <param name="parameter"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract ValueTask<IActionResult> CreateAsync(
ControllerParameter parameter,
CancellationToken cancellationToken);

/// <summary>
/// Perform an update operation in an asynchronous manner.
/// </summary>
/// <param name="parameter"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract ValueTask<IActionResult> UpdateAsync(
ControllerParameter parameter,
CancellationToken cancellationToken);

/// <summary>
/// Perform a partial update operation in an asynchronous manner.
/// </summary>
/// <param name="patch"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract ValueTask<IActionResult> PartialUpdateAsync(
JsonPatchDocument<TEntity>? patch,
CancellationToken cancellationToken);

/// <summary>
/// Perform a delete operation in an asynchronous manner.
/// </summary>
/// <param name="key"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected abstract ValueTask<IActionResult> DeleteAsync(
TEntityKey key,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace Codehard.Common.AspNetCore.Controllers.Parameters;

/// <summary>
/// Represents an abstract parameter for a controller.
/// </summary>
public abstract record ControllerParameter
{
private ControllerParameter()
{
}

/// <summary>
/// Represents a parameter containing JSON data.
/// </summary>
public sealed record Json(JsonDocument JsonDocument) : ControllerParameter;

/// <summary>
/// Represents a parameter containing form data.
/// </summary>
public sealed record Form(IFormCollection FormCollection) : ControllerParameter;

/// <summary>
/// Represents an unknown parameter type.
/// </summary>
public sealed record Unknown(HttpRequest Request) : ControllerParameter;

/// <summary>
/// Match a parameter based on its internal type.
/// </summary>
/// <param name="json">The action to perform if the parameter is of type Json.</param>
/// <param name="form">The action to perform if the parameter is of type Form.</param>
/// <param name="other">The action to perform if the parameter is of type Unknown (optional).</param>
/// <typeparam name="TResult">The result type of the actions.</typeparam>
/// <returns>The result of the matched action.</returns>
/// <exception cref="NotSupportedException">Thrown when the parameter type is not supported.</exception>
public TResult Match<TResult>(
Func<Json, TResult> json,
Func<Form, TResult> form,
Func<Unknown, TResult>? other = default)
{
return this switch
{
Json j => json(j),
Form f => form(f),
Unknown u =>
other is not null
? other.Invoke(u)
: default!,
_ => throw new NotSupportedException($"{this.GetType()} is not supported in this context."),
};
}

/// <summary>
/// Checks if this parameter is <see cref="Json"/>.
/// </summary>
public bool IsJsonParameter => this is Json;

/// <summary>
/// Checks if this parameter is <see cref="Form"/>.
/// </summary>
public bool IsFormParameter => this is Form;

/// <summary>
/// Executes the provided action if this parameter is of type Json.
/// </summary>
/// <param name="action">The action to execute if the parameter is of type Json.</param>
public void IfJson(Action<Json> action)
{
if (IsJsonParameter)
{
action((Json)this);
}
}

/// <summary>
/// Executes the provided action if this parameter is of type Form.
/// </summary>
/// <param name="action">The action to execute if the parameter is of type Form.</param>
public void IfForm(Action<Form> action)
{
if (IsFormParameter)
{
action((Form)this);
}
}

/// <summary>
/// Executes the provided asynchronous action if this parameter is of type Json.
/// </summary>
/// <param name="action">The asynchronous action to execute if the parameter is of type Json.</param>
public async Task IfJsonAsync(Func<Json, Task> action)
{
if (IsJsonParameter)
{
await action((Json)this);
}
}

/// <summary>
/// Executes the provided asynchronous action if this parameter is of type Form.
/// </summary>
/// <param name="action">The asynchronous action to execute if the parameter is of type Form.</param>
public async Task IfFormAsync(Func<Form, Task> action)
{
if (IsJsonParameter)
{
await action((Form)this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Codehard.Common.AspNetCore.Controllers.Parameters;

public record struct GetRequestParameter(string Key, string Value);