Update to latest packages and show examples
This commit is contained in:
+14
-13
@@ -7,28 +7,29 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="AutoMapper" Version="16.1.1" />
|
<PackageVersion Include="AutoMapper" Version="16.1.1" />
|
||||||
|
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
|
||||||
<PackageVersion Include="FluentValidation" Version="12.1.1" />
|
<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.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" 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.PostgreSql" Version="2026.6.8" />
|
||||||
<PackageVersion Include="GerstITS.Data.EntityFramework.SqlServer" Version="2026.6.3" />
|
<PackageVersion Include="GerstITS.Data.EntityFramework.SqlServer" Version="2026.6.3" />
|
||||||
<PackageVersion Include="GerstITS.IoC" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.IoC" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.IoC.DotNetCore" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Job" Version="2026.6.2" />
|
<PackageVersion Include="GerstITS.Job" Version="2026.6.2" />
|
||||||
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.Job.Scheduling" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Mail" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.Mail" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Mapping.AutoMapper" Version="2026.6.2" />
|
<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.System" Version="2026.6.8" />
|
||||||
<PackageVersion Include="GerstITS.TestFramework" Version="2026.7.2" />
|
<PackageVersion Include="GerstITS.TestFramework" Version="2026.7.2" />
|
||||||
<PackageVersion Include="GerstITS.Validation" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.Validation" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Web" Version="2026.7.1" />
|
<PackageVersion Include="GerstITS.Web" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Web.Api" Version="2026.6.27" />
|
<PackageVersion Include="GerstITS.Web.Api" Version="2026.7.12" />
|
||||||
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.3" />
|
<PackageVersion Include="GerstITS.Web.Api.Swagger" Version="2026.7.7" />
|
||||||
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.6.25" />
|
<PackageVersion Include="GerstITS.Web.Rest" Version="2026.7.7" />
|
||||||
<PackageVersion Include="Hocon.Extensions.Configuration" Version="2.0.4" />
|
<PackageVersion Include="Hocon.Extensions.Configuration" Version="2.0.4" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
|
||||||
|
|||||||
@@ -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 System.Diagnostics;
|
||||||
|
using GerstITS.Examples.Api.Endpoints;
|
||||||
|
using GerstITS.Examples.Api.ExceptionHandling;
|
||||||
using GerstITS.Web.Api;
|
using GerstITS.Web.Api;
|
||||||
using GerstITS.Web.Api.Hosting;
|
using GerstITS.Web.Api.Hosting;
|
||||||
using GerstITS.Web.Api.Swagger;
|
using GerstITS.Web.Api.Swagger;
|
||||||
@@ -23,6 +25,7 @@ public class Program
|
|||||||
.Services()
|
.Services()
|
||||||
.Build()
|
.Build()
|
||||||
.IfProduction(app => app.UsePreconfiguredHsts())
|
.IfProduction(app => app.UsePreconfiguredHsts())
|
||||||
|
.UseGlobalExceptionHandling()
|
||||||
.IfDevelopment(app => app.UseDeveloperExceptionPage()
|
.IfDevelopment(app => app.UseDeveloperExceptionPage()
|
||||||
.UsePreconfiguredSwagger())
|
.UsePreconfiguredSwagger())
|
||||||
.UsePreconfiguredCors()
|
.UsePreconfiguredCors()
|
||||||
@@ -32,7 +35,11 @@ public class Program
|
|||||||
.UseAuthorization()
|
.UseAuthorization()
|
||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseSession()
|
.UseSession()
|
||||||
.UseEndpoints(endpoints => endpoints.MapControllers())
|
.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapControllers();
|
||||||
|
endpoints.MapEndpoints();
|
||||||
|
})
|
||||||
.UseRewriteUnknownPathsToIndexSite(ApplicationEnvironment.WebApi.BaseUrl)
|
.UseRewriteUnknownPathsToIndexSite(ApplicationEnvironment.WebApi.BaseUrl)
|
||||||
.UseSystemIndependentStaticFiles()
|
.UseSystemIndependentStaticFiles()
|
||||||
.UseSerilogRequestLogging()
|
.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
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="Gerst ITS" value="https://packages.gerst-it.com/nuget/public/v3/index.json" />
|
<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" />
|
<add key="nuget.org - V3" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
<disabledPackageSources>
|
<disabledPackageSources>
|
||||||
@@ -15,9 +14,6 @@
|
|||||||
<activePackageSource>
|
<activePackageSource>
|
||||||
</activePackageSource>
|
</activePackageSource>
|
||||||
<packageSourceMapping>
|
<packageSourceMapping>
|
||||||
<packageSource key="nuget.org - V2">
|
|
||||||
<package pattern="*" />
|
|
||||||
</packageSource>
|
|
||||||
<packageSource key="nuget.org - V3">
|
<packageSource key="nuget.org - V3">
|
||||||
<package pattern="*" />
|
<package pattern="*" />
|
||||||
</packageSource>
|
</packageSource>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
|||||||
|
|
||||||
| Project | Purpose |
|
| 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.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.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. |
|
| `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` |
|
| `ValidationRuleBase<T>` (FluentValidation) | `Logic/Customers/Validation/CustomerValidationRule.cs`, `Logic/Shared/Validation/IdValidationRule.cs` |
|
||||||
| `IsEMail()` rule builder extension | `CustomerValidationRule.cs` |
|
| `IsEMail()` rule builder extension | `CustomerValidationRule.cs` |
|
||||||
| Automatic validation in fluent controllers (`IValidator`) | All controllers via `FluentApiControllerBase` |
|
| 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
|
### GerstITS.Mapping.AutoMapper
|
||||||
| Feature | Example |
|
| Feature | Example |
|
||||||
@@ -92,7 +93,12 @@ This solution demonstrates all features of the GerstITS framework packages in a
|
|||||||
| `HostingStartup` fluent pipeline | `Api/Program.cs` |
|
| `HostingStartup` fluent pipeline | `Api/Program.cs` |
|
||||||
| `FluentApiControllerBase` (`Api().Use(...).Get/Create/Update/Delete`) | `Api/Controllers/1.1/*.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` |
|
| 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 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` |
|
| 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` |
|
| 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` |
|
| 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"`).
|
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).
|
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
|
### Api key protected endpoints
|
||||||
|
|
||||||
`GET /api/Maintenance/Ping` requires the header configured under `WebApi:ApiKey` (default: `X-Api-Key: example-api-key`).
|
`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