add some default types to de-duplicate similar implmentations

This commit is contained in:
Pilzinsel64
2025-03-28 10:40:48 +01:00
parent 9a652a343c
commit 8de643b7d1
17 changed files with 555 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
using Castle.Core.Logging;
using Pilz.Extensions.Reflection;
using Pilz.Net.Data;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Reflection;
@@ -12,8 +13,12 @@ namespace Pilz.Net.Api;
public class ApiServer : IApiServer
{
protected record struct ThreadHolder(Thread? Thread);
public class MissingDataManagerException : Exception { }
protected readonly List<PrivateMessageHandler> handlers = [];
protected readonly Dictionary<Type, IApiMessageSerializer> serializers = [];
protected readonly Dictionary<ThreadHolder, IDataManager> managers = [];
protected HttpListener httpListener;
protected int restartAttempts = 0;
protected DateTime lastRestartAttempt;
@@ -24,6 +29,8 @@ public class ApiServer : IApiServer
public event OnCheckAuthenticationEventHandler? OnCheckAuthentication;
public event OnCheckContextEventHandler? OnCheckContext;
public event OnCheckContextEventHandler? OnCheckContextCompleted;
public event OnGetNewDataManagerEventHandler? OnGetNewDataManager;
public event DataManagerEventHandler? OnResetDataManager;
protected record PrivateParameterInfo(string Name, int Index);
@@ -51,6 +58,10 @@ public class ApiServer : IApiServer
public int MaxConcurentConnections { get; set; } = 5;
public IDataManager Manager => GetManager();
public bool ThreadedDataManager { get; set; }
public ApiServer(string apiUrl) : this(apiUrl, null)
{
}
@@ -61,16 +72,38 @@ public class ApiServer : IApiServer
this.httpListener = httpListener ?? CreateDefaultHttpListener();
}
private IDataManager GetManager()
{
var curThread = ThreadedDataManager ? Thread.CurrentThread : null;
var threadHolder = new ThreadHolder(curThread);
if (managers.TryGetValue(threadHolder, out var mgr))
return mgr;
if (OnGetNewDataManager?.Invoke(this, EventArgs.Empty) is not IDataManager manager)
throw new MissingDataManagerException();
managers.Add(threadHolder, manager);
return manager;
}
public virtual void ResetManager()
{
if (managers.Remove(new(ThreadedDataManager ? Thread.CurrentThread : null), out var manager))
OnResetDataManager?.Invoke(this, new(manager));
}
protected virtual HttpListener CreateDefaultHttpListener()
{
var httpListener = new HttpListener();
httpListener.TimeoutManager.IdleConnection = new TimeSpan(0, 10, 0);
httpListener.TimeoutManager.IdleConnection = new TimeSpan(0, 2, 0);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
httpListener.TimeoutManager.RequestQueue = new TimeSpan(0, 10, 0);
httpListener.TimeoutManager.HeaderWait = new TimeSpan(0, 10, 0);
httpListener.TimeoutManager.RequestQueue = new TimeSpan(0, 2, 0);
httpListener.TimeoutManager.HeaderWait = new TimeSpan(0, 2, 0);
}
return httpListener;
@@ -296,21 +329,27 @@ public class ApiServer : IApiServer
if (context is not null)
{
Log.Info("Request retrived for " + context.Request.RawUrl);
ResetManager();
OnCheckContext?.Invoke(this, new(context));
try
{
CheckContext(context);
success = true;
}
catch (MissingDataManagerException mdmex)
{
Log.Error("DataManager is not supported by this server instnace!", mdmex);
if (DebugMode) throw;
}
catch (Exception ex)
{
Log.Error("Error checking context", ex);
if (DebugMode)
throw;
if (DebugMode) throw;
}
finally
{
OnCheckContextCompleted?.Invoke(this, new(context));
ResetManager();
}
}

View File

@@ -0,0 +1,34 @@
using Pilz.Net.Api.Messages;
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public abstract class BaseChildItemClient<T>(IApiClient client) : BaseClient<T>(client), IBaseChildItemClient<T> where T : IDataObject
{
public virtual async Task<IEnumerable<T>> GetAll(int parentId)
{
return (await client.SendRequest<GeneralItemMessages<T>.Items>(ApiEndpoint, HttpMethod.Get, new ApiParameterCollection
{
["parent"] = parentId,
})).EnsureOk().Items;
}
public override async Task<T> Save(T item)
{
if (item.Id == 0)
throw new NullReferenceException("Item has no parent yet!");
return await base.Save(item);
}
public virtual async Task<T> Save(T item, int parentId)
{
return (await client.SendRequest<GeneralItemMessages<T>.Item>(
ApiEndpoint,
HttpMethod.Post,
GenerateUpdateMessage(item, true),
new ApiParameterCollection
{
["parent"] = parentId,
})).EnsureOk().Item;
}
}

View File

@@ -0,0 +1,30 @@
using Pilz.Net.Api.Messages;
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public abstract class BaseClient<T>(IApiClient client) : IBaseClient<T> where T : IDataObject
{
public abstract string ApiEndpoint { get; }
public virtual async Task<T> Get(int id)
{
return (await client.SendRequest<GeneralItemMessages<T>.Item>($"{ApiEndpoint}/{id}", HttpMethod.Get)).EnsureOk().Item;
}
public virtual async Task Delete(int id)
{
(await client.SendRequest($"{ApiEndpoint}/{id}", HttpMethod.Delete)).EnsureOk();
}
public virtual async Task<T> Save(T item)
{
var generateNew = item.Id == 0;
return (await client.SendRequest<GeneralItemMessages<T>.Item>(
generateNew ? ApiEndpoint : $"{ApiEndpoint}/{item.Id}",
generateNew ? HttpMethod.Post : HttpMethod.Put,
GenerateUpdateMessage(item, generateNew))).EnsureOk().Item;
}
protected abstract ApiMessage GenerateUpdateMessage(T item, bool generateNew);
}

View File

@@ -0,0 +1,12 @@
using Pilz.Net.Api.Messages;
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public abstract class BaseItemClient<T>(IApiClient client) : BaseClient<T>(client), IBaseItemClient<T> where T : IDataObject
{
public virtual async Task<IEnumerable<T>> GetAll()
{
return (await client.SendRequest<GeneralItemMessages<T>.Items>(ApiEndpoint, HttpMethod.Get)).EnsureOk().Items;
}
}

View File

@@ -0,0 +1,9 @@
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public interface IBaseChildItemClient<T> : IBaseClient<T> where T : IDataObject
{
Task<IEnumerable<T>> GetAll(int parentId);
Task<T> Save(T item, int parentId);
}

View File

@@ -0,0 +1,10 @@
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public interface IBaseClient<T> where T : IDataObject
{
Task Delete(int id);
Task<T> Get(int id);
Task<T> Save(T item);
}

View File

@@ -0,0 +1,8 @@
using Pilz.Net.Data;
namespace Pilz.Net.Api.Client;
public interface IBaseItemClient<T> : IBaseClient<T> where T : IDataObject
{
Task<IEnumerable<T>> GetAll();
}

View File

@@ -1,4 +1,5 @@
using Castle.Core.Logging;
using Pilz.Net.Data;
namespace Pilz.Net.Api;
@@ -6,10 +7,15 @@ public interface IApiServer
{
public delegate void OnCheckAuthenticationEventHandler(object sender, ApiAuthCheckEventArgs e);
public delegate void OnCheckContextEventHandler(object sender, ApiContextEventArgs e);
public delegate IDataManager? OnGetNewDataManagerEventHandler(object sender, EventArgs e);
event OnCheckAuthenticationEventHandler? OnCheckAuthentication;
event OnCheckContextEventHandler OnCheckContext;
event OnCheckContextEventHandler OnCheckContextCompleted;
event OnCheckContextEventHandler? OnCheckContext;
event OnCheckContextEventHandler? OnCheckContextCompleted;
event OnGetNewDataManagerEventHandler? OnGetNewDataManager;
event DataManagerEventHandler? OnResetDataManager;
IDataManager Manager { get; }
string ApiUrl { get; }

View File

@@ -0,0 +1,21 @@
using System.ComponentModel;
namespace Pilz.Net.Api.Messages;
public static class GeneralItemMessages<T>
{
public class Item(T item) : ObjectMessage(item);
public class Items(List<T> items) : ObjectsMessage(items);
[EditorBrowsable(EditorBrowsableState.Never)]
public class ObjectMessage(T item) : ApiMessage
{
public T Item { get; } = item;
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class ObjectsMessage(List<T> items) : ApiMessage
{
public List<T> Items { get; } = items;
}
}

View File

@@ -0,0 +1,52 @@
using Pilz.Extensions.Reflection;
using Pilz.Net.Data;
using Pilz.Net.Extensions;
using System.Diagnostics;
namespace Pilz.Net.Api.Server;
public abstract class BaseChildItemHandler<TEntity, TParent, TUpdateMsg>(IApiServer server)
: BaseHandler<TEntity, TUpdateMsg>(server)
where TEntity : IDataObject
where TParent : IDataObject
where TUpdateMsg : ApiMessage
{
protected virtual bool RegisterGetAll => true;
protected virtual bool RegisterPost => true;
public override void Initialize(IApiServer server)
{
base.Initialize(server);
var t = GetType();
if (RegisterGetAll)
server.RegisterHandler(t.GetMethod(nameof(GetAll))!.CreateDelegate(this), new(Route, "GET"), Debugger.IsAttached);
if (RegisterPost)
server.RegisterHandler(t.GetMethod(nameof(Post))!.CreateDelegate(this), new(Route, "POST"), Debugger.IsAttached);
}
public virtual ApiResult GetAll(int parent)
{
IQueryable<TEntity> list;
if (parent != 0 && server.Manager.Find(parent, out TParent? parentEntity))
list = GetChilds(parentEntity).AsQueryable();
else
list = server.Manager.Get<TEntity>();
return list.ToList().Select(ToClient).ToItemsResult();
}
public virtual ApiResult Post(int parent, TUpdateMsg msg)
{
if (!server.Manager.Find(parent, out TParent? parentEntity))
return ApiResult.NotFound();
var entity = CreateNewEntity();
if (UpdateEntity(entity, msg) is ApiResult result)
return result;
GetChilds(parentEntity).Add(entity);
server.Manager.Save(parentEntity, true);
return ToClient(entity).ToItemResult();
}
public abstract IList<TEntity> GetChilds(TParent parent);
}

View File

@@ -0,0 +1,63 @@
using Pilz.Extensions.Reflection;
using Pilz.Net.Data;
using Pilz.Net.Extensions;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Pilz.Net.Api.Server;
public abstract class BaseHandler<TEntity, TUpdateMsg>(IApiServer server)
: IApiHandlerInitializer
where TEntity : IDataObject
where TUpdateMsg : ApiMessage
{
public abstract string Route { get; }
protected virtual bool RegisterGet => true;
protected virtual bool RegisterPut => true;
protected virtual bool RegisterDelete => true;
public virtual void Initialize(IApiServer server)
{
var t = GetType();
if (RegisterGet)
server.RegisterHandler(t.GetMethod(nameof(Get))!.CreateDelegate(this), new(Route + "/{id}", "GET"), Debugger.IsAttached);
if (RegisterPut)
server.RegisterHandler(t.GetMethod(nameof(Put))!.CreateDelegate(this), new(Route + "/{id}", "PUT"), Debugger.IsAttached);
if (RegisterDelete)
server.RegisterHandler(t.GetMethod(nameof(Delete))!.CreateDelegate(this), new(Route + "/{id}", "DELETE"), Debugger.IsAttached);
}
public virtual ApiResult Get(int id)
{
if (!server.Manager.Find(id, out TEntity? entity))
return ApiResult.NotFound();
return ToClient(entity).ToItemResult();
}
public virtual ApiResult Put(int id, TUpdateMsg msg)
{
if (!server.Manager.Find(id, out TEntity? entity))
return ApiResult.NotFound();
if (UpdateEntity(entity, msg) is ApiResult result)
return result;
server.Manager.Save(entity, true);
return ToClient(entity).ToItemResult();
}
public virtual ApiResult Delete(int id)
{
server.Manager.Delete<TEntity>(id, true);
return ApiResult.Ok();
}
protected virtual TEntity CreateNewEntity()
{
return Activator.CreateInstance<TEntity>();
}
protected abstract ApiResult? UpdateEntity(TEntity entity, TUpdateMsg msg);
protected abstract IDataObject ToClient(TEntity entity);
}

View File

@@ -0,0 +1,39 @@
using Pilz.Extensions.Reflection;
using Pilz.Net.Data;
using Pilz.Net.Extensions;
using System.Diagnostics;
namespace Pilz.Net.Api.Server;
public abstract class BaseItemHandler<TEntity, TUpdateMsg>(IApiServer server)
: BaseHandler<TEntity, TUpdateMsg>(server)
where TEntity : IDataObject
where TUpdateMsg : ApiMessage
{
protected virtual bool RegisterGetAll => true;
protected virtual bool RegisterPost => true;
public override void Initialize(IApiServer server)
{
base.Initialize(server);
var t = GetType();
if (RegisterGetAll)
server.RegisterHandler(t.GetMethod(nameof(GetAll))!.CreateDelegate(this), new(Route, "GET"), Debugger.IsAttached);
if (RegisterPost)
server.RegisterHandler(t.GetMethod(nameof(Put))!.CreateDelegate(this), new(Route, "POST"), Debugger.IsAttached);
}
public virtual ApiResult GetAll()
{
return server.Manager.Get<TEntity>().ToList().Select(ToClient).ToItemsResult();
}
public virtual ApiResult Post(TUpdateMsg msg)
{
var entity = CreateNewEntity();
if (UpdateEntity(entity, msg) is ApiResult result)
return result;
server.Manager.Save(entity, true);
return entity.ToItemResult();
}
}

View File

@@ -0,0 +1,149 @@
using System.Diagnostics.CodeAnalysis;
namespace Pilz.Net.Data;
public abstract class DataManager : IDataManager
{
protected abstract void UpdateEntity<T>(T obj) where T : IDataObject;
protected abstract IQueryable<T> GetEntitySet<T>() where T : IDataObject;
protected abstract T? FindEntity<T>(int id) where T : IDataObject;
protected abstract void RemoveEntity<T>(T obj) where T : IDataObject;
protected abstract void SaveChanges();
public virtual IQueryable<T> Get<T>() where T : IDataObject
{
return GetEntitySet<T>();
}
public virtual T FindOrNew<T>(int? id) where T : IDataObject
{
if (Find<T>(id) is not T obj)
obj = Activator.CreateInstance<T>();
return obj;
}
public virtual T FindOrNew<T>(int id) where T : IDataObject
{
if (Find<T>(id) is not T obj)
obj = Activator.CreateInstance<T>();
return obj;
}
public virtual T? FindOrNull<T>(int? id) where T : IDataObject
{
if (id == null || id == 0)
return default;
return Find<T>(id);
}
public virtual T? FindOrNull<T>(int id) where T : IDataObject
{
if (id == 0)
return default;
return Find<T>(id);
}
public virtual T? Find<T>(int? id) where T : IDataObject
{
if (id == null)
return default;
return Find<T>(id.Value);
}
public virtual T? Find<T>(int id) where T : IDataObject
{
return FindEntity<T>(id);
}
public virtual bool FindOrNew<T>(int? id, [NotNullWhen(true)] out T? obj) where T : IDataObject
{
if (id == null || id == 0)
{
obj = Activator.CreateInstance<T>();
return obj != null;
}
return Find(id.Value, out obj);
}
public virtual bool FindOrNew<T>(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject
{
if (id == 0)
{
obj = Activator.CreateInstance<T>();
return obj != null;
}
return Find(id, out obj);
}
public virtual bool FindOrNull<T>(int? id, out T? obj) where T : IDataObject
{
if (id == null || id == 0)
{
obj = default;
return true;
}
return Find(id.Value, out obj);
}
public virtual bool FindOrNull<T>(int id, out T? obj) where T : IDataObject
{
if (id == 0)
{
obj = default;
return true;
}
return Find(id, out obj);
}
public virtual bool Find<T>(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject
{
if (FindEntity<T>(id) is not T t)
{
obj = default;
return false;
}
obj = t;
return true;
}
public virtual void Delete<T>(int id) where T : IDataObject
{
Delete<T>(id, false);
}
public virtual void Delete<T>(int id, bool save) where T : IDataObject
{
if (Find(id, out T? obj))
Delete(obj, save);
}
public virtual void Delete<T>(T? obj) where T : IDataObject
{
Delete(obj, false);
}
public virtual void Delete<T>(T? obj, bool save) where T : IDataObject
{
if (obj != null)
{
RemoveEntity(obj);
if (save) Save();
}
}
public virtual void Save<T>(T obj) where T : IDataObject
{
Save(obj, false);
}
public virtual void Save<T>(T obj, bool save) where T : IDataObject
{
UpdateEntity(obj);
if (save) Save();
}
public virtual void Save()
{
SaveChanges();
}
}

View File

@@ -0,0 +1,8 @@
namespace Pilz.Net.Data;
public class DataManagerEventArgs(IDataManager manager) : EventArgs
{
public IDataManager Manager { get; } = manager;
}
public delegate void DataManagerEventHandler(object sender, DataManagerEventArgs e);

View File

@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
namespace Pilz.Net.Data;
public interface IDataManager
{
void Delete<T>(int id) where T : IDataObject;
void Delete<T>(int id, bool save) where T : IDataObject;
void Delete<T>(T? obj) where T : IDataObject;
void Delete<T>(T? obj, bool save) where T : IDataObject;
T? Find<T>(int id) where T : IDataObject;
bool Find<T>(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject;
T? Find<T>(int? id) where T : IDataObject;
T FindOrNew<T>(int id) where T : IDataObject;
bool FindOrNew<T>(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject;
T FindOrNew<T>(int? id) where T : IDataObject;
bool FindOrNew<T>(int? id, [NotNullWhen(true)] out T? obj) where T : IDataObject;
T? FindOrNull<T>(int id) where T : IDataObject;
bool FindOrNull<T>(int id, out T? obj) where T : IDataObject;
T? FindOrNull<T>(int? id) where T : IDataObject;
bool FindOrNull<T>(int? id, out T? obj) where T : IDataObject;
IQueryable<T> Get<T>() where T : IDataObject;
void Save();
void Save<T>(T obj) where T : IDataObject;
void Save<T>(T obj, bool save) where T : IDataObject;
}

View File

@@ -0,0 +1,6 @@
namespace Pilz.Net.Data;
public interface IDataObject
{
public int Id { get; }
}

View File

@@ -0,0 +1,36 @@
using Pilz.Net.Api;
using Pilz.Net.Api.Messages;
namespace Pilz.Net.Extensions;
public static class ApiMessageExtensions
{
public static ApiResult ToItemResult<T>(this T? @this)
{
if (@this == null)
return ApiResult.NotFound();
return ApiResult.Ok(@this.ToItemMsg());
}
public static ApiResult ToItemsResult<T>(this IEnumerable<T>? @this)
{
if (@this == null)
return ApiResult.NotFound();
return ApiResult.Ok(@this.ToItemsMsg());
}
public static GeneralItemMessages<T>.Item ToItemMsg<T>(this T @this)
{
return new GeneralItemMessages<T>.Item(@this);
}
public static GeneralItemMessages<T>.Items ToItemsMsg<T>(this IEnumerable<T> @this)
{
return new GeneralItemMessages<T>.Items([.. @this]);
}
public static GeneralItemMessages<T>.Items ToItemsMsg<T>(this T[] @this)
{
return new GeneralItemMessages<T>.Items([.. @this]);
}
}