Update to latest packages and show examples
This commit is contained in:
+15
-14
@@ -7,28 +7,29 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||
<PackageVersion Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageVersion Include="GerstITS.Authentication.ApiKey" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Authentication.ApiKey" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Authentication.OpenId" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Common" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Common" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Data" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework" Version="2026.7.8" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.PostgreSql" Version="2026.6.8" />
|
||||
<PackageVersion Include="GerstITS.Data.EntityFramework.SqlServer" Version="2026.6.3" />
|
||||
<PackageVersion Include="GerstITS.IoC" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.IoC" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Job" Version="2026.6.2" />
|
||||
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Mail" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Mail" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Mapping.AutoMapper" Version="2026.6.2" />
|
||||
<PackageVersion Include="GerstITS.Search" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Search" Version="2026.7.10" />
|
||||
<PackageVersion Include="GerstITS.System" Version="2026.6.8" />
|
||||
<PackageVersion Include="GerstITS.TestFramework" Version="2026.7.2" />
|
||||
<PackageVersion Include="GerstITS.Validation" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Web" Version="2026.7.1" />
|
||||
<PackageVersion Include="GerstITS.Web.Api" Version="2026.6.27" />
|
||||
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.3" />
|
||||
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.6.25" />
|
||||
<PackageVersion Include="GerstITS.Validation" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Web" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Web.Api" Version="2026.7.12" />
|
||||
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.7" />
|
||||
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.7.7" />
|
||||
<PackageVersion Include="Hocon.Extensions.Configuration" Version="2.0.4" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||
@@ -44,4 +45,4 @@
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using GerstITS.Authentication.ApiKey;
|
||||
using GerstITS.System.Configurations;
|
||||
|
||||
namespace GerstITS.Examples.Api.Configurations;
|
||||
|
||||
internal class ApiKeyConfiguration : IApiKeyConfiguration
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public ApiKeyConfiguration(string parentPrefix,
|
||||
Microsoft.Extensions.Configuration.IConfiguration configuration)
|
||||
{
|
||||
var prefix = $"{parentPrefix}:{this.ToConfigurationPrefix()}";
|
||||
|
||||
Header = configuration.GetValue<string>($"{prefix}:{nameof(Header)}");
|
||||
Key = configuration.GetValue<string>($"{prefix}:{nameof(Key)}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IApiKeyConfiguration
|
||||
|
||||
public string Header { get; }
|
||||
public string Key { get; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Net;
|
||||
using GerstITS.Web.Api.ExceptionHandling;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace GerstITS.Examples.Api.ExceptionHandling;
|
||||
|
||||
internal static class WebApplicationExtensions
|
||||
{
|
||||
internal static WebApplication UseGlobalExceptionHandling(this WebApplication app)
|
||||
{
|
||||
app.UseExceptionHandler(exceptionHandlerApp =>
|
||||
{
|
||||
exceptionHandlerApp.Run(async context =>
|
||||
{
|
||||
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
|
||||
if (exceptionFeature is null) return;
|
||||
|
||||
var exception = exceptionFeature.Error;
|
||||
var ticketId = Guid.NewGuid().ToString();
|
||||
|
||||
context.RequestServices
|
||||
.GetRequiredService<ILoggerFactory>()
|
||||
.CreateLogger("GlobalExceptionHandler")
|
||||
.LogError($"Global exception occurs (TicketId {ticketId}).{Environment.NewLine}{exception}");
|
||||
|
||||
var transformation = context.RequestServices
|
||||
.GetServices<IExceptionTransformation>()
|
||||
.FirstOrDefault(x => x.CanTransform(exception));
|
||||
|
||||
var info = transformation?.Transform(exception, ticketId) ?? new ExceptionTransformationInfo
|
||||
{
|
||||
StatusCode = HttpStatusCode.InternalServerError,
|
||||
ReasonPhrase = "Internal Server Error.",
|
||||
Details = $"An internal error occurs. Please contact support with ticket id '{ticketId}'."
|
||||
};
|
||||
|
||||
context.Response.StatusCode = (int)info.StatusCode;
|
||||
context.Features.Get<IHttpResponseFeature>().ReasonPhrase = info.ReasonPhrase;
|
||||
await context.Response.WriteAsync(info.Details ?? string.Empty);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Asp.Versioning;
|
||||
using GerstITS.Examples.Api.Versioning;
|
||||
using GerstITS.System.Caching;
|
||||
using GerstITS.Validation;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.FluentApi;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Controllers._1._1;
|
||||
|
||||
/// <summary>
|
||||
/// Is responsible to demonstrate the encrypted session based cache.
|
||||
/// </summary>
|
||||
[ApiController,
|
||||
ApiVersion(Versions._1_1),
|
||||
Route(ApplicationEnvironment.WebApi.ControllerRouteTemplate)]
|
||||
public class CacheController : FluentApiControllerBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ICache _cache;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CacheController(ICache cache,
|
||||
IValidator validator)
|
||||
: base(validator)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached value by specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the cached value.</param>
|
||||
/// <returns>Returns the cached value.</returns>
|
||||
[HttpGet,
|
||||
Route("{key}")]
|
||||
public IActionResult Get(string key)
|
||||
{
|
||||
return Api().Use(key)
|
||||
.Get(_cache.Get<string>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a value is cached for the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the cached value.</param>
|
||||
/// <returns>Returns true if a value is cached.</returns>
|
||||
[HttpGet,
|
||||
Route("{key}/Exists")]
|
||||
public IActionResult Exists(string key)
|
||||
{
|
||||
return Api().Use(key)
|
||||
.Get(_cache.ContainsKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches a value for the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the value to cache.</param>
|
||||
/// <param name="value">Value to cache.</param>
|
||||
/// <returns>Returns the cached value.</returns>
|
||||
[HttpPost,
|
||||
Route("{key}")]
|
||||
public IActionResult Set(string key, [FromBody] string value)
|
||||
{
|
||||
return Api().Use(key)
|
||||
.Create(x => {
|
||||
_cache.Set(x, value);
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a cached value by specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">Key of the cached value.</param>
|
||||
/// <returns>Returns true if the value was removed.</returns>
|
||||
[HttpDelete,
|
||||
Route("{key}")]
|
||||
public IActionResult Remove(string key)
|
||||
{
|
||||
return Api().Use(key)
|
||||
.Delete(x => {
|
||||
_cache.Remove(x);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Asp.Versioning;
|
||||
using GerstITS.Examples.Api.Versioning;
|
||||
using GerstITS.Mail;
|
||||
using GerstITS.Validation;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.FluentApi;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Controllers._1._1;
|
||||
|
||||
/// <summary>
|
||||
/// Is responsible to send contact requests via the configured smtp mail provider.
|
||||
/// </summary>
|
||||
[ApiController,
|
||||
ApiVersion(Versions._1_1),
|
||||
Route(ApplicationEnvironment.WebApi.ControllerRouteTemplate)]
|
||||
public class ContactController : FluentApiControllerBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private const string _supportAddress = "support@example.com";
|
||||
|
||||
private readonly IMailProvider _mailProvider;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public ContactController(IMailProvider mailProvider,
|
||||
IValidator validator)
|
||||
: base(validator)
|
||||
{
|
||||
_mailProvider = mailProvider;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Sends a contact request as mail to the support address.
|
||||
/// </summary>
|
||||
/// <param name="contactRequest">Contact request to send.</param>
|
||||
/// <returns>Returns true if the mail was sent.</returns>
|
||||
[HttpPost]
|
||||
public IActionResult Send([FromBody] ContactRequest contactRequest)
|
||||
{
|
||||
return Api().Use(contactRequest)
|
||||
.Create(x => _mailProvider.SendMail(ToMailNotification(x)));
|
||||
}
|
||||
|
||||
private static MailNotification ToMailNotification(ContactRequest contactRequest)
|
||||
{
|
||||
return new MailNotification
|
||||
{
|
||||
Recipients = new[] { _supportAddress },
|
||||
Subject = $"{contactRequest.Subject} ({contactRequest.SenderName}, {contactRequest.SenderEmail})",
|
||||
Body = contactRequest.Body,
|
||||
IsHtmlMail = false
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Asp.Versioning;
|
||||
using GerstITS.Data;
|
||||
using GerstITS.Examples.Api.Versioning;
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Search;
|
||||
using GerstITS.Validation;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.FluentApi;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Controllers._1._1;
|
||||
|
||||
/// <summary>
|
||||
/// Is responsible to manage customers backed by the repository based data access.
|
||||
/// </summary>
|
||||
[ApiController,
|
||||
ApiVersion(Versions._1_1),
|
||||
Route(ApplicationEnvironment.WebApi.ControllerRouteTemplate)]
|
||||
public class CustomerController : FluentApiControllerBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ICustomerProvider _provider;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CustomerController(ICustomerProvider provider,
|
||||
IValidator validator)
|
||||
: base(validator)
|
||||
{
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets a customer by specified id. Deactivated customers are excluded by default.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the customer.</param>
|
||||
/// <param name="includeInactive">Includes deactivated (soft deleted) customers if set to true.</param>
|
||||
/// <returns>Returns a customer.</returns>
|
||||
[HttpGet,
|
||||
Route("{id}")]
|
||||
public IActionResult Get(int id, [FromQuery] bool includeInactive = false)
|
||||
{
|
||||
return Api().Use(id)
|
||||
.Get(x => _provider.GetById(x, includeInactive));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches customers by specified filter using the search engine (paging, sorting, filtering).
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter criteria of the search.</param>
|
||||
/// <returns>Returns the matching customers including the total count.</returns>
|
||||
[HttpPost,
|
||||
Route("Search")]
|
||||
public IActionResult Search([FromBody] CustomerSearchFilter filter)
|
||||
{
|
||||
return Api().Use(filter)
|
||||
.Get(_provider.Search);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new customer.
|
||||
/// </summary>
|
||||
/// <param name="customer">Customer to create.</param>
|
||||
/// <returns>Returns the created customer.</returns>
|
||||
[HttpPost]
|
||||
public IActionResult Create([FromBody] Customer customer)
|
||||
{
|
||||
return Api().Use(customer)
|
||||
.Create(_provider.Create);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing customer.
|
||||
/// </summary>
|
||||
/// <param name="customer">Customer to update.</param>
|
||||
/// <returns>Returns the updated customer.</returns>
|
||||
[HttpPut]
|
||||
public IActionResult Update([FromBody] Customer customer)
|
||||
{
|
||||
return Api().Use(customer)
|
||||
.Update(_provider.Update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a customer. Performs a soft delete by default, a permanent delete if force is set to true.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the customer.</param>
|
||||
/// <param name="force">Deletes the customer permanently if set to true.</param>
|
||||
/// <returns>Returns true if the customer was deleted.</returns>
|
||||
[HttpDelete,
|
||||
Route("{id}")]
|
||||
public IActionResult Delete(int id, [FromQuery] bool force = false)
|
||||
{
|
||||
return Api().Use(id)
|
||||
.Delete(x => _provider.Delete(x, force));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Imports customers using bulk operations.
|
||||
/// </summary>
|
||||
/// <param name="customers">Customers to import.</param>
|
||||
/// <returns>Returns the bulk operation result.</returns>
|
||||
[HttpPost,
|
||||
Route("Import")]
|
||||
public IActionResult Import([FromBody] IEnumerable<Customer> customers)
|
||||
{
|
||||
return Api().Use(customers)
|
||||
.Create(_provider.Import);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all notes of a customer ordered by their validity.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the customer.</param>
|
||||
/// <returns>Returns the notes of the customer.</returns>
|
||||
[HttpGet,
|
||||
Route("{id}/Notes")]
|
||||
public IActionResult GetNotes(int id)
|
||||
{
|
||||
return Api().Use(id)
|
||||
.Get(_provider.GetNotes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a historicized note to a customer.
|
||||
/// </summary>
|
||||
/// <param name="id">Id of the customer.</param>
|
||||
/// <param name="note">Note to add.</param>
|
||||
/// <returns>Returns the created note.</returns>
|
||||
[HttpPost,
|
||||
Route("{id}/Notes")]
|
||||
public IActionResult AddNote(int id, [FromBody] CustomerNote note)
|
||||
{
|
||||
return Api().Use(id)
|
||||
.Create(x => _provider.AddNote(x, note.Text));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Asp.Versioning;
|
||||
using GerstITS.Authentication.ApiKey;
|
||||
using GerstITS.Examples.Api.Versioning;
|
||||
using GerstITS.System.Environment;
|
||||
using GerstITS.Validation;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.FluentApi;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Controllers._1._1;
|
||||
|
||||
/// <summary>
|
||||
/// Is responsible to provide maintenance functions protected by an api key header.
|
||||
/// </summary>
|
||||
[ApiController,
|
||||
ApiVersion(Versions._1_1),
|
||||
ApiKey,
|
||||
Route(ApplicationEnvironment.WebApi.ControllerRouteTemplate)]
|
||||
public class MaintenanceController : FluentApiControllerBase
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ISystemClock _systemClock;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public MaintenanceController(ISystemClock systemClock,
|
||||
IValidator validator)
|
||||
: base(validator)
|
||||
{
|
||||
_systemClock = systemClock;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current server time. Requires a valid api key header.
|
||||
/// </summary>
|
||||
/// <returns>Returns the current server time.</returns>
|
||||
[HttpGet,
|
||||
Route("Ping")]
|
||||
public IActionResult Ping()
|
||||
{
|
||||
return Api().Get(() => _systemClock.UtcNow);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Asp.Versioning;
|
||||
using GerstITS.Examples.Api.Versioning;
|
||||
using GerstITS.Validation;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.FluentApi;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Controllers._1._1;
|
||||
|
||||
/// <summary>
|
||||
/// Is responsible to demonstrate open id (jwt bearer) protected endpoints.
|
||||
/// </summary>
|
||||
[ApiController,
|
||||
ApiVersion(Versions._1_1),
|
||||
Authorize,
|
||||
Route(ApplicationEnvironment.WebApi.ControllerRouteTemplate)]
|
||||
public class SecuredController : FluentApiControllerBase
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public SecuredController(IValidator validator)
|
||||
: base(validator)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the authenticated user. Requires a valid bearer token.
|
||||
/// </summary>
|
||||
/// <returns>Returns the name of the authenticated user.</returns>
|
||||
[HttpGet,
|
||||
Route("Me")]
|
||||
public IActionResult Me()
|
||||
{
|
||||
return Api().Get(() => User.Identity.Name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Endpoints;
|
||||
|
||||
internal static partial class Endpoints
|
||||
{
|
||||
internal static RouteGroupBuilder MapCustomerNoteEndpoints(this RouteGroupBuilder group)
|
||||
{
|
||||
CustomerNotes.MapGetByCustomer(group);
|
||||
CustomerNotes.MapAdd(group);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
internal static class CustomerNotes
|
||||
{
|
||||
internal static void MapGetByCustomer(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("customers/{id:int}/notes", (int id, ICustomerProvider customerProvider) =>
|
||||
Results.Ok(customerProvider.GetNotes(id)));
|
||||
}
|
||||
|
||||
internal static void MapAdd(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("customers/{id:int}/notes", (int id, [FromBody] CustomerNote note, ICustomerProvider customerProvider) =>
|
||||
Results.Ok(customerProvider.AddNote(id, note.Text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GerstITS.Examples.Api.Endpoints;
|
||||
|
||||
internal static partial class Endpoints
|
||||
{
|
||||
internal static RouteGroupBuilder MapCustomerEndpoints(this IEndpointRouteBuilder root)
|
||||
{
|
||||
var group = root.MapGroup("/api/v2.0")
|
||||
.WithTags("Customers");
|
||||
|
||||
Customers.MapGetById(group);
|
||||
Customers.MapSearch(group);
|
||||
Customers.MapCreate(group);
|
||||
Customers.MapUpdate(group);
|
||||
Customers.MapDelete(group);
|
||||
Customers.MapImport(group);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
internal static class Customers
|
||||
{
|
||||
internal static void MapGetById(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("customers/{id:int}", (int id, ICustomerProvider customerProvider, bool includeInactive = false) =>
|
||||
Results.Ok(customerProvider.GetById(id, includeInactive)));
|
||||
}
|
||||
|
||||
internal static void MapSearch(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapGet("customers", ([AsParameters] CustomerSearchFilter filter, ICustomerProvider customerProvider, IValidator validator) =>
|
||||
{
|
||||
Validator.Validate(validator, filter);
|
||||
|
||||
return Results.Ok(customerProvider.Search(filter));
|
||||
});
|
||||
}
|
||||
|
||||
internal static void MapCreate(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("customers", ([FromBody] Customer customer, ICustomerProvider customerProvider, IValidator validator) =>
|
||||
{
|
||||
Validator.Validate(validator, customer);
|
||||
|
||||
return Results.Ok(customerProvider.Create(customer));
|
||||
});
|
||||
}
|
||||
|
||||
internal static void MapUpdate(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPut("customers/{id:int}", (int id, [FromBody] Customer customer, ICustomerProvider customerProvider, IValidator validator) =>
|
||||
{
|
||||
customer.Id = id;
|
||||
Validator.Validate(validator, customer);
|
||||
|
||||
return Results.Ok(customerProvider.Update(customer));
|
||||
});
|
||||
}
|
||||
|
||||
internal static void MapDelete(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapDelete("customers/{id:int}", (int id, ICustomerProvider customerProvider, bool force = false) =>
|
||||
Results.Ok(customerProvider.Delete(id, force)));
|
||||
}
|
||||
|
||||
internal static void MapImport(RouteGroupBuilder group)
|
||||
{
|
||||
group.MapPost("customers/import", ([FromBody] IEnumerable<Customer> customers, ICustomerProvider customerProvider) =>
|
||||
Results.Ok(customerProvider.Import(customers)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using GerstITS.Validation;
|
||||
|
||||
namespace GerstITS.Examples.Api.Endpoints;
|
||||
|
||||
internal static partial class Endpoints
|
||||
{
|
||||
internal static class Validator
|
||||
{
|
||||
internal static void Validate<T>(IValidator validator, T model)
|
||||
{
|
||||
var result = validator.Validate(model);
|
||||
|
||||
if (!result.IsValid)
|
||||
throw new ValidationException($"{typeof(T).Name} is invalid.{Environment.NewLine}Details: {result.ToErrorMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GerstITS.Examples.Api.Endpoints;
|
||||
|
||||
internal static partial class Endpoints
|
||||
{
|
||||
internal static RouteGroupBuilder MapEndpoints(this IEndpointRouteBuilder root)
|
||||
{
|
||||
return root.MapCustomerEndpoints()
|
||||
.MapCustomerNoteEndpoints();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using GerstITS.Web.Http;
|
||||
|
||||
namespace GerstITS.Examples.Api;
|
||||
|
||||
public sealed partial class Module
|
||||
{
|
||||
#region Methods
|
||||
|
||||
private static void RegisterSession(IServiceCollection container)
|
||||
{
|
||||
container.AddDistributedMemoryCache();
|
||||
container.AddHttpSession();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using GerstITS.Examples.Api.Endpoints;
|
||||
using GerstITS.Examples.Api.ExceptionHandling;
|
||||
using GerstITS.Web.Api;
|
||||
using GerstITS.Web.Api.Hosting;
|
||||
using GerstITS.Web.Api.Swagger;
|
||||
@@ -23,6 +25,7 @@ public class Program
|
||||
.Services()
|
||||
.Build()
|
||||
.IfProduction(app => app.UsePreconfiguredHsts())
|
||||
.UseGlobalExceptionHandling()
|
||||
.IfDevelopment(app => app.UseDeveloperExceptionPage()
|
||||
.UsePreconfiguredSwagger())
|
||||
.UsePreconfiguredCors()
|
||||
@@ -32,7 +35,11 @@ public class Program
|
||||
.UseAuthorization()
|
||||
.UseRouting()
|
||||
.UseSession()
|
||||
.UseEndpoints(endpoints => endpoints.MapControllers())
|
||||
.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapEndpoints();
|
||||
})
|
||||
.UseRewriteUnknownPathsToIndexSite(ApplicationEnvironment.WebApi.BaseUrl)
|
||||
.UseSystemIndependentStaticFiles()
|
||||
.UseSerilogRequestLogging()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GerstITS.Examples.Data;
|
||||
|
||||
public sealed class CustomerDbContext : EntityFrameworkDbContextBase, IDbContext
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public DbSet<CustomerEntity> Customers => Set<CustomerEntity>();
|
||||
public DbSet<CustomerNoteEntity> CustomerNotes => Set<CustomerNoteEntity>();
|
||||
|
||||
protected override string ConnectionStringName => "CustomerDbContext";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CustomerDbContext(IDatabaseConfigurator databaseConfigurator,
|
||||
DbContextOptions<CustomerDbContext> dbContextOptions)
|
||||
: base(databaseConfigurator, dbContextOptions)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using GerstITS.Data;
|
||||
|
||||
namespace GerstITS.Examples.Data.Entities;
|
||||
|
||||
public sealed class CustomerEntity : EntityBase<int>, IActivatable, IReadOnlyActivatable, ICreatable, IReadOnlyCreatable, IModifiable, IReadOnlyModifiable, IVersionable, IReadOnlyVersionable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string EMail { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime? Deactivated { get; set; }
|
||||
public string DeactivatedBy { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public string CreatedBy { get; set; }
|
||||
|
||||
public DateTime? Modified { get; set; }
|
||||
public string ModifiedBy { get; set; }
|
||||
|
||||
public int Version { get; set; }
|
||||
|
||||
public ICollection<CustomerNoteEntity> Notes { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using GerstITS.Data;
|
||||
|
||||
namespace GerstITS.Examples.Data.Entities;
|
||||
|
||||
public sealed class CustomerNoteEntity : EntityBase<int>, ICreatable, IReadOnlyCreatable, IHistoricizable, IReadOnlyHistoricizable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
public string Text { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public string CreatedBy { get; set; }
|
||||
|
||||
public string HistorySet { get; set; }
|
||||
public DateTime ValidFrom { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Company>Gerst ITS</Company>
|
||||
<Authors>Gerst ITS</Authors>
|
||||
<Copyright>© 2025 Gerst ITS</Copyright>
|
||||
<Product>Gerst ITS Examples data access</Product>
|
||||
<Description>Example data access.</Description>
|
||||
<Version>0.0.0.0</Version>
|
||||
<AssemblyVersion>0.0.0.0</AssemblyVersion>
|
||||
<AssemblyInformationalVersion>0.0.0.0</AssemblyInformationalVersion>
|
||||
<FileVersion>0.0.0.0</FileVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn />
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GerstITS.Data" />
|
||||
<PackageReference Include="GerstITS.Data.EntityFramework" />
|
||||
<PackageReference Include="GerstITS.Data.EntityFramework.PostgreSql" />
|
||||
<PackageReference Include="GerstITS.IoC" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectCapability Include="ConfigurableFileNesting" />
|
||||
<ProjectCapability Include="ConfigurableFileNestingFeatureEnabled" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace GerstITS.Examples.Data.Mappings;
|
||||
|
||||
internal sealed class CustomerEntityMapping : EntityMappingBase<CustomerEntity>
|
||||
{
|
||||
#region Properties
|
||||
|
||||
protected override string TableName => "Customers";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override void AddKeyMapping(EntityTypeBuilder<CustomerEntity> entity)
|
||||
{
|
||||
entity.HasKey(i => i.Id);
|
||||
}
|
||||
|
||||
protected override void AddPropertyMappings(EntityTypeBuilder<CustomerEntity> entity)
|
||||
{
|
||||
entity.Property(i => i.FirstName).HasMaxLength(100).IsRequired();
|
||||
entity.Property(i => i.LastName).HasMaxLength(100).IsRequired();
|
||||
entity.Property(i => i.EMail).HasMaxLength(250).IsRequired();
|
||||
|
||||
entity.Property(i => i.CreatedBy).HasMaxLength(100);
|
||||
entity.Property(i => i.ModifiedBy).HasMaxLength(100);
|
||||
entity.Property(i => i.DeactivatedBy).HasMaxLength(100);
|
||||
|
||||
entity.Property(i => i.Version).IsConcurrencyToken();
|
||||
}
|
||||
|
||||
protected override void AddNavigationProperties(EntityTypeBuilder<CustomerEntity> entity)
|
||||
{
|
||||
entity.HasMany(i => i.Notes)
|
||||
.WithOne()
|
||||
.HasForeignKey(i => i.CustomerId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace GerstITS.Examples.Data.Mappings;
|
||||
|
||||
internal sealed class CustomerNoteEntityMapping : EntityMappingBase<CustomerNoteEntity>
|
||||
{
|
||||
#region Properties
|
||||
|
||||
protected override string TableName => "CustomerNotes";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override void AddKeyMapping(EntityTypeBuilder<CustomerNoteEntity> entity)
|
||||
{
|
||||
entity.HasKey(i => i.Id);
|
||||
}
|
||||
|
||||
protected override void AddPropertyMappings(EntityTypeBuilder<CustomerNoteEntity> entity)
|
||||
{
|
||||
entity.Property(i => i.Text).HasMaxLength(2000).IsRequired();
|
||||
|
||||
entity.Property(i => i.CreatedBy).HasMaxLength(100);
|
||||
entity.Property(i => i.HistorySet).HasMaxLength(50);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using GerstITS.Examples.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerstITS.Examples.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CustomerDbContext))]
|
||||
[Migration("20260703100412_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("examples")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityAlwaysColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("Deactivated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeactivatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("EMail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("character varying(250)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("Modified")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ModifiedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers", "examples");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerNoteEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("CustomerId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("HistorySet")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("ValidFrom")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("CustomerNotes", "examples");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerNoteEntity", b =>
|
||||
{
|
||||
b.HasOne("GerstITS.Examples.Data.Entities.CustomerEntity", null)
|
||||
.WithMany("Notes")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerEntity", b =>
|
||||
{
|
||||
b.Navigation("Notes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerstITS.Examples.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "examples");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Customers",
|
||||
schema: "examples",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn),
|
||||
FirstName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
LastName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
EMail = table.Column<string>(type: "character varying(250)", maxLength: 250, nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Deactivated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
DeactivatedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
Modified = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
ModifiedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
Version = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Customers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustomerNotes",
|
||||
schema: "examples",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn),
|
||||
CustomerId = table.Column<int>(type: "integer", nullable: false),
|
||||
Text = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false),
|
||||
Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
HistorySet = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
ValidFrom = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CustomerNotes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomerNotes_Customers_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalSchema: "examples",
|
||||
principalTable: "Customers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomerNotes_CustomerId",
|
||||
schema: "examples",
|
||||
table: "CustomerNotes",
|
||||
column: "CustomerId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustomerNotes",
|
||||
schema: "examples");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Customers",
|
||||
schema: "examples");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using GerstITS.Data.EntityFramework.PostgreSql.Migrations;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace GerstITS.Examples.Data.Migrations;
|
||||
|
||||
public sealed class CustomerDatabaseConfigurator : PostgreSqlDatabaseConfiguratorBase<CustomerDbContext>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public CustomerDatabaseConfigurator(IConfiguration configuration)
|
||||
: base("examples", configuration)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using GerstITS.Examples.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace GerstITS.Examples.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CustomerDbContext))]
|
||||
partial class CustomerDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("examples")
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityAlwaysColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("Deactivated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeactivatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("EMail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("character varying(250)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime?>("Modified")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ModifiedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers", "examples");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerNoteEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("CustomerId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("HistorySet")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("ValidFrom")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("CustomerNotes", "examples");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerNoteEntity", b =>
|
||||
{
|
||||
b.HasOne("GerstITS.Examples.Data.Entities.CustomerEntity", null)
|
||||
.WithMany("Notes")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("GerstITS.Examples.Data.Entities.CustomerEntity", b =>
|
||||
{
|
||||
b.Navigation("Notes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
|
||||
namespace GerstITS.Examples.Data.Migrations.DesignTime;
|
||||
|
||||
public sealed class CustomerDbContextDesignTimeFactory : DesignTimeDbContextFactoryBase<CustomerDbContext>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using GerstITS.Data.EntityFramework;
|
||||
using GerstITS.Examples.Data.Migrations;
|
||||
using GerstITS.IoC;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GerstITS.Examples.Data;
|
||||
|
||||
public sealed class Module : IIoCModule<IServiceCollection>
|
||||
{
|
||||
#region IIoCModule
|
||||
|
||||
public void RegisterComponents(IServiceCollection container)
|
||||
{
|
||||
container.AddMigration<CustomerDbContext, CustomerDatabaseConfigurator>();
|
||||
container.AddEntityFrameworkSession<CustomerDbContext>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"CustomerDbContext": "Host=localhost;Port=5432;Database=gerstits_examples;Username=postgres;Password=postgres"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using GerstITS.Examples.Logic.Shared.Search.Sorting;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
public class Customer : ISortable
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string EMail { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
public class CustomerNote
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public int Id { get; set; }
|
||||
public string Text { get; set; }
|
||||
public DateTime ValidFrom { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using GerstITS.Search;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
public class CustomerSearchFilter : SearchFilterBase
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public string Name { get; set; }
|
||||
public string EMail { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using GerstITS.Data;
|
||||
using GerstITS.Search;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
public interface ICustomerProvider
|
||||
{
|
||||
Customer GetById(int id);
|
||||
Customer GetById(int id, bool includeInactive);
|
||||
|
||||
SearchResult<Customer> Search(CustomerSearchFilter filter);
|
||||
|
||||
Customer Create(Customer customer);
|
||||
Customer Update(Customer customer);
|
||||
|
||||
bool Delete(int id);
|
||||
bool Delete(int id, bool forceDelete);
|
||||
|
||||
BulkResult Import(IEnumerable<Customer> customers);
|
||||
|
||||
IEnumerable<CustomerNote> GetNotes(int customerId);
|
||||
CustomerNote AddNote(int customerId, string text);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using AutoMapper;
|
||||
using GerstITS.Common;
|
||||
using GerstITS.Data;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
using GerstITS.Search;
|
||||
using GerstITS.System.Customizations;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
internal sealed class CustomerProvider : ICustomerProvider
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly IRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ISearchEngine _searchEngine;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CustomerProvider(IRepository repository,
|
||||
IMapper mapper,
|
||||
ISearchEngine searchEngine)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
_searchEngine = searchEngine;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ICustomerProvider
|
||||
|
||||
public Customer GetById(int id)
|
||||
{
|
||||
return GetById(id, false);
|
||||
}
|
||||
|
||||
public Customer GetById(int id, bool includeInactive)
|
||||
{
|
||||
var entity = _repository.Query<CustomerEntity>(includeInactive)
|
||||
.FirstOrDefault(i => i.Id == id)
|
||||
.ThrowsAnExceptionIfNotExisting();
|
||||
|
||||
return ToCustomizedModel(entity);
|
||||
}
|
||||
|
||||
public SearchResult<Customer> Search(CustomerSearchFilter filter)
|
||||
{
|
||||
var searchResult = _searchEngine.Search<Customer, CustomerSearchFilter>(filter);
|
||||
|
||||
searchResult.Result = searchResult.Result
|
||||
.Select(i => i.ApplyCustomizations())
|
||||
.ToList();
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
public Customer Create(Customer customer)
|
||||
{
|
||||
var entity = _mapper.Map<CustomerEntity>(customer);
|
||||
|
||||
_repository.Save(entity);
|
||||
|
||||
return ToCustomizedModel(entity);
|
||||
}
|
||||
|
||||
public Customer Update(Customer customer)
|
||||
{
|
||||
var entity = _repository.Query<CustomerEntity>()
|
||||
.FirstOrDefault(i => i.Id == customer.Id)
|
||||
.ThrowsAnExceptionIfNotExisting();
|
||||
|
||||
_mapper.Map(customer, entity);
|
||||
|
||||
_repository.Save(entity);
|
||||
|
||||
return ToCustomizedModel(entity);
|
||||
}
|
||||
|
||||
public bool Delete(int id)
|
||||
{
|
||||
return Delete(id, false);
|
||||
}
|
||||
|
||||
public bool Delete(int id, bool forceDelete)
|
||||
{
|
||||
var entity = _repository.Query<CustomerEntity>(forceDelete)
|
||||
.FirstOrDefault(i => i.Id == id)
|
||||
.ThrowsAnExceptionIfNotExisting();
|
||||
|
||||
_repository.Delete(entity, forceDelete);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public BulkResult Import(IEnumerable<Customer> customers)
|
||||
{
|
||||
var importableCustomers = customers.Is(x => x.IsNullOrEmpty())
|
||||
.ThenThrows<ArgumentException>()
|
||||
.WithMessage("At least one customer is required for an import.");
|
||||
|
||||
var entities = importableCustomers.Select(_mapper.Map<CustomerEntity>)
|
||||
.ToList();
|
||||
|
||||
return _repository.BulkSave(entities);
|
||||
}
|
||||
|
||||
public IEnumerable<CustomerNote> GetNotes(int customerId)
|
||||
{
|
||||
return _repository.Query<CustomerNoteEntity>()
|
||||
.Where(i => i.CustomerId == customerId)
|
||||
.OrderBy(i => i.ValidFrom)
|
||||
.ToList()
|
||||
.Select(_mapper.Map<CustomerNote>)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public CustomerNote AddNote(int customerId, string text)
|
||||
{
|
||||
_repository.Query<CustomerEntity>()
|
||||
.FirstOrDefault(i => i.Id == customerId)
|
||||
.ThrowsAnExceptionIfNotExisting();
|
||||
|
||||
var entity = new CustomerNoteEntity
|
||||
{
|
||||
CustomerId = customerId,
|
||||
Text = text
|
||||
};
|
||||
|
||||
_repository.Save(entity);
|
||||
|
||||
return _mapper.Map<CustomerNote>(entity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
private Customer ToCustomizedModel(CustomerEntity entity)
|
||||
{
|
||||
return _mapper.Map<Customer>(entity)
|
||||
.ApplyCustomizations();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using GerstITS.Common;
|
||||
using GerstITS.System.Customizations;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers.Customizations;
|
||||
|
||||
internal sealed class CustomerDisplayNameCustomization : CustomizationBase<Customer>
|
||||
{
|
||||
#region Methods
|
||||
|
||||
protected override bool CanCustomize(Customer item)
|
||||
{
|
||||
return item.IsNotNull();
|
||||
}
|
||||
|
||||
protected override Customer Customize(Customer item)
|
||||
{
|
||||
item.DisplayName = $"{item.LastName}, {item.FirstName}";
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using AutoMapper;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers;
|
||||
|
||||
internal sealed class CustomerMapping : Profile
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public CustomerMapping()
|
||||
{
|
||||
CreateMap<CustomerEntity, Customer>()
|
||||
.ForMember(x => x.DisplayName, m => m.Ignore());
|
||||
|
||||
CreateMap<Customer, CustomerEntity>()
|
||||
.ForMember(x => x.Created, m => m.Ignore())
|
||||
.ForMember(x => x.CreatedBy, m => m.Ignore())
|
||||
.ForMember(x => x.Modified, m => m.Ignore())
|
||||
.ForMember(x => x.ModifiedBy, m => m.Ignore())
|
||||
.ForMember(x => x.Deactivated, m => m.Ignore())
|
||||
.ForMember(x => x.DeactivatedBy, m => m.Ignore())
|
||||
.ForMember(x => x.Version, m => m.Ignore())
|
||||
.ForMember(x => x.Notes, m => m.Ignore());
|
||||
|
||||
CreateMap<CustomerNoteEntity, CustomerNote>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using GerstITS.Common;
|
||||
using GerstITS.Search;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers.Search;
|
||||
|
||||
internal sealed class CustomerSearchFilterInitializationRule : InitializationRuleBase<CustomerSearchFilter>
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private const int _defaultTake = 50;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override CustomerSearchFilter InitializeInstance(CustomerSearchFilter instance)
|
||||
{
|
||||
instance.Skip ??= 0;
|
||||
instance.Take ??= _defaultTake;
|
||||
|
||||
if (instance.SortBy.IsNullOrWhiteSpace())
|
||||
instance.SortBy = nameof(Customer.LastName);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using GerstITS.Common;
|
||||
using GerstITS.Data;
|
||||
using GerstITS.Examples.Data.Entities;
|
||||
using GerstITS.Search;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers.Search;
|
||||
|
||||
internal sealed class CustomerSearchQueryBuilderRule : SearchQueryBuilderRuleBase<Customer, CustomerSearchFilter>
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly IReadOnlyRepository _repository;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CustomerSearchQueryBuilderRule(IReadOnlyRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
protected override IQueryable<Customer> CreateQuery(CustomerSearchFilter filter)
|
||||
{
|
||||
var query = _repository.Query<CustomerEntity>();
|
||||
|
||||
if (filter.Name.IsNotNullOrWhiteSpace())
|
||||
query = query.Where(i => i.FirstName.Contains(filter.Name)
|
||||
|| i.LastName.Contains(filter.Name));
|
||||
|
||||
if (filter.EMail.IsNotNullOrWhiteSpace())
|
||||
query = query.Where(i => i.EMail == filter.EMail);
|
||||
|
||||
return query.Project(x => new Customer
|
||||
{
|
||||
Id = x.Id,
|
||||
FirstName = x.FirstName,
|
||||
LastName = x.LastName,
|
||||
EMail = x.EMail,
|
||||
IsActive = x.IsActive
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using GerstITS.Search;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers.Validation;
|
||||
|
||||
internal sealed class CustomerSearchFilterValidationRule : PagingValidationRuleBase<CustomerSearchFilter>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
using GerstITS.Validation;
|
||||
|
||||
namespace GerstITS.Examples.Logic.Customers.Validation;
|
||||
|
||||
internal sealed class CustomerValidationRule : ValidationRuleBase<Customer>
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
public CustomerValidationRule()
|
||||
{
|
||||
RuleFor(x => x.FirstName)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.LastName)
|
||||
.NotEmpty();
|
||||
|
||||
RuleFor(x => x.EMail)
|
||||
.NotEmpty()
|
||||
.IsEMail();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Examples.Logic.Customers.Customizations;
|
||||
using GerstITS.Examples.Logic.Customers.Search;
|
||||
using GerstITS.Examples.Logic.Customers.Validation;
|
||||
using GerstITS.Mapping.AutoMapper;
|
||||
using GerstITS.Search;
|
||||
using GerstITS.System.Customizations;
|
||||
using GerstITS.Validation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GerstITS.Examples.Logic;
|
||||
|
||||
public sealed partial class Module
|
||||
{
|
||||
#region Methods
|
||||
|
||||
private static void RegisterCustomers(IServiceCollection container)
|
||||
{
|
||||
container.RegisterMappingsOf<ICustomerProvider>();
|
||||
container.RegisterValidationRulesOf<CustomerValidationRule>();
|
||||
container.RegisterSearchQueryBuilderRulesOf<CustomerSearchQueryBuilderRule>();
|
||||
container.RegisterInitializationRulesOf<CustomerSearchFilterInitializationRule>();
|
||||
container.RegisterCustomizationsOf<CustomerDisplayNameCustomization>();
|
||||
|
||||
container.AddScoped<ICustomerProvider, CustomerProvider>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using AwesomeAssertions;
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Examples.Logic.Customers.Customizations;
|
||||
using GerstITS.System.Customizations;
|
||||
using GerstITS.TestFramework;
|
||||
using Xunit;
|
||||
|
||||
namespace GerstITS.Examples.Tests.Customers.Customizations;
|
||||
|
||||
[TestCategory(TestCategories.Unit)]
|
||||
public sealed class CustomerDisplayNameCustomizationTest
|
||||
{
|
||||
#region Tests
|
||||
|
||||
[Fact]
|
||||
public void If_a_customer_is_customized___The_display_name_is_composed()
|
||||
{
|
||||
ICustomization customization = new CustomerDisplayNameCustomization();
|
||||
var customer = new Customer { FirstName = "Erika", LastName = "Mustermann" };
|
||||
|
||||
var customized = customization.Customize(customer, null);
|
||||
|
||||
customized.DisplayName.Should().Be("Mustermann, Erika");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_customer_is_null___It_cannot_be_customized()
|
||||
{
|
||||
ICustomization customization = new CustomerDisplayNameCustomization();
|
||||
|
||||
customization.CanCustomize<Customer>(null, null).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using AwesomeAssertions;
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Examples.Logic.Customers.Search;
|
||||
using GerstITS.TestFramework;
|
||||
using Xunit;
|
||||
|
||||
namespace GerstITS.Examples.Tests.Customers.Search;
|
||||
|
||||
[TestCategory(TestCategories.Unit)]
|
||||
public sealed class CustomerSearchFilterInitializationRuleTest
|
||||
{
|
||||
#region Tests
|
||||
|
||||
[Fact]
|
||||
public void If_the_filter_is_empty___Defaults_are_applied()
|
||||
{
|
||||
var filter = new CustomerSearchFilter();
|
||||
|
||||
var initialized = new CustomerSearchFilterInitializationRule().Initialize(filter);
|
||||
|
||||
initialized.Skip.Should().Be(0);
|
||||
initialized.Take.Should().Be(50);
|
||||
initialized.SortBy.Should().Be(nameof(Customer.LastName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_filter_is_already_configured___Values_are_kept()
|
||||
{
|
||||
var filter = new CustomerSearchFilter
|
||||
{
|
||||
Skip = 10,
|
||||
Take = 5,
|
||||
SortBy = nameof(Customer.EMail)
|
||||
};
|
||||
|
||||
var initialized = new CustomerSearchFilterInitializationRule().Initialize(filter);
|
||||
|
||||
initialized.Skip.Should().Be(10);
|
||||
initialized.Take.Should().Be(5);
|
||||
initialized.SortBy.Should().Be(nameof(Customer.EMail));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_another_filter_type_is_checked___It_cannot_be_initialized()
|
||||
{
|
||||
new CustomerSearchFilterInitializationRule().CanInitialize<string>().Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using AwesomeAssertions;
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Examples.Logic.Customers.Validation;
|
||||
using GerstITS.TestFramework;
|
||||
using Xunit;
|
||||
|
||||
namespace GerstITS.Examples.Tests.Customers.Validation;
|
||||
|
||||
[TestCategory(TestCategories.Unit)]
|
||||
public sealed class CustomerValidationRuleTest
|
||||
{
|
||||
#region Tests
|
||||
|
||||
[Fact]
|
||||
public void If_a_customer_is_complete___It_is_valid()
|
||||
{
|
||||
var customer = CreateCustomer();
|
||||
|
||||
var result = new CustomerValidationRule().Validate((object)customer);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_first_name_is_missing___It_is_invalid()
|
||||
{
|
||||
var customer = CreateCustomer();
|
||||
customer.FirstName = string.Empty;
|
||||
|
||||
var result = new CustomerValidationRule().Validate((object)customer);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_email_has_no_valid_format___It_is_invalid()
|
||||
{
|
||||
var customer = CreateCustomer();
|
||||
customer.EMail = "not-an-email";
|
||||
|
||||
var result = new CustomerValidationRule().Validate((object)customer);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_a_customer_type_is_checked___It_can_be_validated()
|
||||
{
|
||||
new CustomerValidationRule().CanValidate(typeof(Customer)).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
private static Customer CreateCustomer()
|
||||
{
|
||||
return new Customer
|
||||
{
|
||||
FirstName = "Erika",
|
||||
LastName = "Mustermann",
|
||||
EMail = "erika.mustermann@example.com"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GerstITS.TestFramework" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="AwesomeAssertions" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GerstITS.Examples.Logic\GerstITS.Examples.Logic.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,57 @@
|
||||
using AwesomeAssertions;
|
||||
using GerstITS.Examples.Logic.Customers;
|
||||
using GerstITS.Examples.Logic.Shared.Search.Sorting;
|
||||
using GerstITS.Search;
|
||||
using GerstITS.TestFramework;
|
||||
using Xunit;
|
||||
|
||||
namespace GerstITS.Examples.Tests.Shared.Search.Sorting;
|
||||
|
||||
[TestCategory(TestCategories.Unit)]
|
||||
public sealed class SortingProviderTest
|
||||
{
|
||||
#region Tests
|
||||
|
||||
[Fact]
|
||||
public void If_customers_are_sorted_descending___The_order_is_reversed()
|
||||
{
|
||||
var query = CreateCustomers().AsQueryable();
|
||||
var sorting = new CustomerSearchFilter { SortBy = nameof(Customer.LastName), SortDirection = SortingDirections.Descending };
|
||||
|
||||
var sorted = new SortingProvider().Sort(query, sorting);
|
||||
|
||||
sorted.First().LastName.Should().Be("Zimmermann");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_sort_property_is_unknown___The_query_is_left_unchanged()
|
||||
{
|
||||
var query = CreateCustomers().AsQueryable();
|
||||
var sorting = new CustomerSearchFilter { SortBy = "Unknown" };
|
||||
|
||||
var sorted = new SortingProvider().Sort(query, sorting);
|
||||
|
||||
sorted.Last().LastName.Should().Be("Zimmermann");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_item_type_is_not_sortable___It_cannot_be_sorted()
|
||||
{
|
||||
new SortingProvider().CanSort(new[] { "a" }.AsQueryable()).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
private static IEnumerable<Customer> CreateCustomers()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new Customer { FirstName = "Anna", LastName = "Albrecht" },
|
||||
new Customer { FirstName = "Zoe", LastName = "Zimmermann" }
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using AwesomeAssertions;
|
||||
using GerstITS.Examples.Logic.Shared.Validation;
|
||||
using GerstITS.TestFramework;
|
||||
using Xunit;
|
||||
|
||||
namespace GerstITS.Examples.Tests.Shared.Validation;
|
||||
|
||||
[TestCategory(TestCategories.Unit)]
|
||||
public sealed class IdValidationRuleTest
|
||||
{
|
||||
#region Tests
|
||||
|
||||
[Fact]
|
||||
public void If_the_id_is_positive___It_is_valid()
|
||||
{
|
||||
var result = new IdValidationRule().Validate((object)1);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void If_the_id_is_zero___It_is_invalid()
|
||||
{
|
||||
var result = new IdValidationRule().Validate((object)0);
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using GerstITS.Common;
|
||||
using GerstITS.Examples.WebClients.Examples.Api;
|
||||
using GerstITS.IoC.DotNetCore;
|
||||
using GerstITS.System.Environment;
|
||||
using GerstITS.System.Json;
|
||||
using GerstITS.System.Reflection;
|
||||
|
||||
namespace GerstITS.Examples.WebClient.Console.Tests;
|
||||
|
||||
internal sealed class CommonFeaturesRunner : IApplicationStart
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ISystemClock _systemClock;
|
||||
private readonly IJsonConvert _jsonConvert;
|
||||
private readonly IActivator _activator;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CommonFeaturesRunner(ISystemClock systemClock,
|
||||
IJsonConvert jsonConvert,
|
||||
IActivator activator)
|
||||
{
|
||||
_systemClock = systemClock;
|
||||
_jsonConvert = jsonConvert;
|
||||
_activator = activator;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IApplicationStart
|
||||
|
||||
public void Run()
|
||||
{
|
||||
RunStringExtensions();
|
||||
RunObjectExtensions();
|
||||
RunEnumerableExtensions();
|
||||
RunRanges();
|
||||
RunFluentExceptionHandling();
|
||||
RunCryptography();
|
||||
RunSystemAbstractions();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
private static void RunStringExtensions()
|
||||
{
|
||||
Print($"---> ToCamelCase: {"HelloWorld".ToCamelCase()}");
|
||||
Print($"---> ToValue<int>: {"42".ToValue<int>()}");
|
||||
Print($"---> ToValue<TimeSpan> with default: {"invalid".ToValue(TimeSpan.FromMinutes(5))}");
|
||||
Print($"---> IsNullOrWhiteSpace: {" ".IsNullOrWhiteSpace()}");
|
||||
Print($"---> ReplaceFirstOccurrence: {"a-a-a".ReplaceFirstOccurrence("a", "b")}");
|
||||
Print($"---> GetDeterministicHashCode: {"stable".GetDeterministicHashCode()}");
|
||||
}
|
||||
|
||||
private static void RunObjectExtensions()
|
||||
{
|
||||
object nothing = null;
|
||||
|
||||
Print($"---> IsNull: {nothing.IsNull()}, IsNotNull: {"value".IsNotNull()}");
|
||||
Print($"---> Enum Parse: {"descending".Parse<SortingDirections>()}");
|
||||
}
|
||||
|
||||
private static void RunEnumerableExtensions()
|
||||
{
|
||||
var numbers = new[] { 1, 2, 2, 3 };
|
||||
|
||||
Print($"---> IsNotNullOrEmpty: {numbers.IsNotNullOrEmpty()}");
|
||||
Print($"---> AsEnumerable: {1.AsEnumerable().Count()}");
|
||||
|
||||
numbers.Distinct()
|
||||
.ForEach(i => Print($"---> ForEach: {i}"));
|
||||
}
|
||||
|
||||
private static void RunRanges()
|
||||
{
|
||||
var january = new Range<DateTime>(new DateTime(2026, 1, 1), new DateTime(2026, 1, 31));
|
||||
var midJanuaryToFebruary = new Range<DateTime>(new DateTime(2026, 1, 15), new DateTime(2026, 2, 15));
|
||||
|
||||
Print($"---> Range intersects: {january.Intersects(midJanuaryToFebruary)}");
|
||||
}
|
||||
|
||||
private static void RunFluentExceptionHandling()
|
||||
{
|
||||
try
|
||||
{
|
||||
(-1).Is(x => x < 0)
|
||||
.ThenThrows<ArgumentOutOfRangeException>()
|
||||
.WithMessage(x => $"The value '{x}' must not be negative.");
|
||||
}
|
||||
catch (ArgumentOutOfRangeException exception)
|
||||
{
|
||||
Print($"---> Fluent exception handling: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunCryptography()
|
||||
{
|
||||
var encrypted = "A secret value".Encrypt();
|
||||
|
||||
Print($"---> Encrypt/Decrypt roundtrip: {encrypted.Decrypt()}");
|
||||
}
|
||||
|
||||
private void RunSystemAbstractions()
|
||||
{
|
||||
Print($"---> ISystemClock.UtcNow: {_systemClock.UtcNow}");
|
||||
Print($"---> IJsonConvert.Serialize: {_jsonConvert.Serialize(new { Name = "Example" })}");
|
||||
Print($"---> IActivator.CreateInstance: {_activator.CreateInstance<Range<int>>(0, 10).End}");
|
||||
}
|
||||
|
||||
private static void Print(string message)
|
||||
{
|
||||
global::System.Console.WriteLine(message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using GerstITS.Web.WebClients;
|
||||
|
||||
namespace GerstITS.Examples.WebClients.Examples.Api;
|
||||
|
||||
public interface ICustomer : IWebService
|
||||
{
|
||||
[WebMethod(WebMethods.Get),
|
||||
ResourceUrl("Customer/{id}"),
|
||||
HttpHeader(Name = "X-Client", Value = "GerstITS.Examples.WebClient")]
|
||||
Customer Get(int id);
|
||||
|
||||
[WebMethod(WebMethods.Post),
|
||||
ResourceUrl("Customer/Search")]
|
||||
SearchResult<Customer> Search(CustomerSearchCriteria criteria);
|
||||
|
||||
[WebMethod(WebMethods.Post),
|
||||
ResourceUrl("Customer")]
|
||||
Customer Create(Customer model);
|
||||
|
||||
[WebMethod(WebMethods.Put),
|
||||
ResourceUrl("Customer")]
|
||||
Customer Update(Customer model);
|
||||
|
||||
[WebMethod(WebMethods.Delete),
|
||||
ResourceUrl("Customer/{id}")]
|
||||
bool Delete(int id);
|
||||
|
||||
[WebMethod(WebMethods.Get),
|
||||
ResourceUrl("Customer/{id}/Notes")]
|
||||
IEnumerable<CustomerNote> GetNotes(int id);
|
||||
|
||||
[WebMethod(WebMethods.Post),
|
||||
ResourceUrl("Customer/{id}/Notes")]
|
||||
CustomerNote AddNote(int id, CustomerNote note);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace GerstITS.Examples.WebClients.Examples.Api;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string EMail { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace GerstITS.Examples.WebClients.Examples.Api;
|
||||
|
||||
public class CustomerNote
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public int Id { get; set; }
|
||||
public string Text { get; set; }
|
||||
public DateTime ValidFrom { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace GerstITS.Examples.WebClients.Examples.Api;
|
||||
|
||||
public class CustomerSearchCriteria
|
||||
{
|
||||
#region Properties
|
||||
|
||||
public string Name { get; set; }
|
||||
public string EMail { get; set; }
|
||||
|
||||
public int? Skip { get; set; }
|
||||
public int? Take { get; set; }
|
||||
|
||||
public string SortBy { get; set; }
|
||||
public SortingDirections SortDirection { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
+1
-5
@@ -7,7 +7,6 @@
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Gerst ITS" value="https://packages.gerst-it.com/nuget/public/v3/index.json" />
|
||||
<add key="nuget.org - V2" value="https://www.nuget.org/api/v2/" />
|
||||
<add key="nuget.org - V3" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<disabledPackageSources>
|
||||
@@ -15,10 +14,7 @@
|
||||
<activePackageSource>
|
||||
</activePackageSource>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org - V2">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="nuget.org - V3">
|
||||
<packageSource key="nuget.org - V3">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="Gerst ITS">
|
||||
|
||||
@@ -6,7 +6,7 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
||||
|
||||
| Project | Purpose |
|
||||
|---------|---------|
|
||||
| `GerstITS.Examples.Api` | ASP.NET Core Web API host: fluent controllers, versioning, exception handling, Swagger, authentication, session cache, jobs and auto migration. |
|
||||
| `GerstITS.Examples.Api` | ASP.NET Core Web API host: fluent controllers, minimal API endpoints, versioning, exception handling, Swagger, authentication, session cache, jobs and auto migration. |
|
||||
| `GerstITS.Examples.Logic` | Business logic: providers, search engine integration, validation rules, customizations and AutoMapper profiles. |
|
||||
| `GerstITS.Examples.Data` | Data access: entities with framework aspects, EF Core DbContext, entity mappings, database configurator and migrations. |
|
||||
| `GerstITS.Examples.Jobs.SayHelloWorld` | Scheduled Quartz jobs with cron configuration. |
|
||||
@@ -80,6 +80,7 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
||||
| `ValidationRuleBase<T>` (FluentValidation) | `Logic/Customers/Validation/CustomerValidationRule.cs`, `Logic/Shared/Validation/IdValidationRule.cs` |
|
||||
| `IsEMail()` rule builder extension | `CustomerValidationRule.cs` |
|
||||
| Automatic validation in fluent controllers (`IValidator`) | All controllers via `FluentApiControllerBase` |
|
||||
| Explicit validation in minimal API endpoints (`IValidator` + `ToErrorMessage`) | `Api/Endpoints/Endpoints.Validator.cs` |
|
||||
|
||||
### GerstITS.Mapping.AutoMapper
|
||||
| Feature | Example |
|
||||
@@ -92,7 +93,12 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
||||
| `HostingStartup` fluent pipeline | `Api/Program.cs` |
|
||||
| `FluentApiControllerBase` (`Api().Use(...).Get/Create/Update/Delete`) | `Api/Controllers/1.1/*.cs` |
|
||||
| API versioning (side by side controllers) | `Api/Controllers/1.0` vs `Api/Controllers/1.1` |
|
||||
| Minimal API endpoints (partial `Endpoints` class per feature, nested static endpoint classes) | `Api/Endpoints/Endpoints.cs`, `Endpoints.Customers.cs`, `Endpoints.CustomerNotes.cs` |
|
||||
| Endpoint grouping + Swagger tags (`MapGroup`, `WithTags`) | `Api/Endpoints/Endpoints.Customers.cs` |
|
||||
| Query parameter binding to search filters (`[AsParameters]`) | `Endpoints.Customers.MapSearch` |
|
||||
| Global exception transformations | `Api/Common/ExceptionHandling/Transformations/*.cs` |
|
||||
| Global exception handling middleware for minimal APIs (`UseGlobalExceptionHandling`) | `Api/Common/ExceptionHandling/Extensions/WebApplicationExtensions.cs`, `Api/Program.cs` |
|
||||
| Automatic response compression (zstd, brotli, gzip — also for HTTPS) | built in, no app code required — see "Response compression" below |
|
||||
| Server configuration (CORS, HSTS, forwarded headers) | `Api/Common/Configurations/WebApiConfiguration.Server.*.cs` |
|
||||
| Swagger configuration incl. security schemes | `Api/Common/Configurations/WebApiConfiguration.Swagger*.cs`, `WebApiConfiguration.OpenApiSecurityScheme.cs` |
|
||||
| Session based cache storage (`AddHttpSession`) | `Api/Module.Session.cs`, `CacheController.cs` |
|
||||
@@ -133,6 +139,32 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
||||
4. **Tests**: `dotnet test` (optionally `--filter "Category=Unit"`).
|
||||
5. **Migrations**: `dotnet ef migrations add <Name>` inside `GerstITS.Examples.Data` (uses the design time factory and `appsettings.json` of that project).
|
||||
|
||||
### Fluent controllers vs. minimal API endpoints
|
||||
|
||||
The customer domain is exposed twice to compare both styles:
|
||||
|
||||
- `POST/GET/PUT/DELETE /api/Customer` — fluent controllers (`FluentApiControllerBase`, validation and exception transformations applied by the fluent pipeline).
|
||||
- `GET/POST/PUT/DELETE /api/v2.0/customers` — minimal API endpoints (partial `Endpoints` class, explicit `Endpoints.Validator`, exception transformations applied by `UseGlobalExceptionHandling`).
|
||||
|
||||
Search differs on purpose: the controller uses `POST /api/Customer/Search` with a body filter, the minimal API uses
|
||||
`GET /api/v2.0/customers` binding the same `CustomerSearchFilter` from query parameters via `[AsParameters]`.
|
||||
|
||||
### Response compression
|
||||
|
||||
`GerstITS.Web.Api` enables response compression automatically — there is nothing to register or configure in the
|
||||
application: the framework module registers the zstd (`ZstdSharp`), brotli and gzip providers with
|
||||
`EnableForHttps = true` and `HostingStartup...Build()` adds the `UseResponseCompression()` middleware. The provider is
|
||||
picked by the client's `Accept-Encoding` header (zstd preferred, then brotli, then gzip):
|
||||
|
||||
```
|
||||
curl -s -D - -o NUL -H "Accept-Encoding: zstd, br, gzip" https://localhost:<port>/api/v2.0/customers
|
||||
```
|
||||
|
||||
The response then carries the matching `Content-Encoding` header (e.g. `Content-Encoding: zstd`).
|
||||
|
||||
> Requires the first `GerstITS.Web.Api` release after 2026.7.7 — older packages do not contain the compression
|
||||
> feature yet, requests are then answered uncompressed.
|
||||
|
||||
### Api key protected endpoints
|
||||
|
||||
`GET /api/Maintenance/Ping` requires the header configured under `WebApi:ApiKey` (default: `X-Api-Key: example-api-key`).
|
||||
|
||||
Reference in New Issue
Block a user