diff --git a/Pilz.Net/Api/ApiServer.cs b/Pilz.Net/Api/ApiServer.cs index 4845f75..2c433e3 100644 --- a/Pilz.Net/Api/ApiServer.cs +++ b/Pilz.Net/Api/ApiServer.cs @@ -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 handlers = []; protected readonly Dictionary serializers = []; + protected readonly Dictionary 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(); } } diff --git a/Pilz.Net/Api/Client/BaseChildItemClient.cs b/Pilz.Net/Api/Client/BaseChildItemClient.cs new file mode 100644 index 0000000..0e8b2c7 --- /dev/null +++ b/Pilz.Net/Api/Client/BaseChildItemClient.cs @@ -0,0 +1,34 @@ +using Pilz.Net.Api.Messages; +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public abstract class BaseChildItemClient(IApiClient client) : BaseClient(client), IBaseChildItemClient where T : IDataObject +{ + public virtual async Task> GetAll(int parentId) + { + return (await client.SendRequest.Items>(ApiEndpoint, HttpMethod.Get, new ApiParameterCollection + { + ["parent"] = parentId, + })).EnsureOk().Items; + } + + public override async Task Save(T item) + { + if (item.Id == 0) + throw new NullReferenceException("Item has no parent yet!"); + return await base.Save(item); + } + + public virtual async Task Save(T item, int parentId) + { + return (await client.SendRequest.Item>( + ApiEndpoint, + HttpMethod.Post, + GenerateUpdateMessage(item, true), + new ApiParameterCollection + { + ["parent"] = parentId, + })).EnsureOk().Item; + } +} diff --git a/Pilz.Net/Api/Client/BaseClient.cs b/Pilz.Net/Api/Client/BaseClient.cs new file mode 100644 index 0000000..41a72f3 --- /dev/null +++ b/Pilz.Net/Api/Client/BaseClient.cs @@ -0,0 +1,30 @@ +using Pilz.Net.Api.Messages; +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public abstract class BaseClient(IApiClient client) : IBaseClient where T : IDataObject +{ + public abstract string ApiEndpoint { get; } + + public virtual async Task Get(int id) + { + return (await client.SendRequest.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 Save(T item) + { + var generateNew = item.Id == 0; + return (await client.SendRequest.Item>( + generateNew ? ApiEndpoint : $"{ApiEndpoint}/{item.Id}", + generateNew ? HttpMethod.Post : HttpMethod.Put, + GenerateUpdateMessage(item, generateNew))).EnsureOk().Item; + } + + protected abstract ApiMessage GenerateUpdateMessage(T item, bool generateNew); +} diff --git a/Pilz.Net/Api/Client/BaseItemClient.cs b/Pilz.Net/Api/Client/BaseItemClient.cs new file mode 100644 index 0000000..eb97d97 --- /dev/null +++ b/Pilz.Net/Api/Client/BaseItemClient.cs @@ -0,0 +1,12 @@ +using Pilz.Net.Api.Messages; +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public abstract class BaseItemClient(IApiClient client) : BaseClient(client), IBaseItemClient where T : IDataObject +{ + public virtual async Task> GetAll() + { + return (await client.SendRequest.Items>(ApiEndpoint, HttpMethod.Get)).EnsureOk().Items; + } +} diff --git a/Pilz.Net/Api/Client/IBaseChildItemClient.cs b/Pilz.Net/Api/Client/IBaseChildItemClient.cs new file mode 100644 index 0000000..058c364 --- /dev/null +++ b/Pilz.Net/Api/Client/IBaseChildItemClient.cs @@ -0,0 +1,9 @@ +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public interface IBaseChildItemClient : IBaseClient where T : IDataObject +{ + Task> GetAll(int parentId); + Task Save(T item, int parentId); +} diff --git a/Pilz.Net/Api/Client/IBaseClient.cs b/Pilz.Net/Api/Client/IBaseClient.cs new file mode 100644 index 0000000..cdfd231 --- /dev/null +++ b/Pilz.Net/Api/Client/IBaseClient.cs @@ -0,0 +1,10 @@ +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public interface IBaseClient where T : IDataObject +{ + Task Delete(int id); + Task Get(int id); + Task Save(T item); +} diff --git a/Pilz.Net/Api/Client/IBaseItemClient.cs b/Pilz.Net/Api/Client/IBaseItemClient.cs new file mode 100644 index 0000000..4d58b7b --- /dev/null +++ b/Pilz.Net/Api/Client/IBaseItemClient.cs @@ -0,0 +1,8 @@ +using Pilz.Net.Data; + +namespace Pilz.Net.Api.Client; + +public interface IBaseItemClient : IBaseClient where T : IDataObject +{ + Task> GetAll(); +} diff --git a/Pilz.Net/Api/IApiServer.cs b/Pilz.Net/Api/IApiServer.cs index d3dd7b4..cf31e6e 100644 --- a/Pilz.Net/Api/IApiServer.cs +++ b/Pilz.Net/Api/IApiServer.cs @@ -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; } diff --git a/Pilz.Net/Api/Messages/GeneralItemMessages.cs b/Pilz.Net/Api/Messages/GeneralItemMessages.cs new file mode 100644 index 0000000..e48e0f0 --- /dev/null +++ b/Pilz.Net/Api/Messages/GeneralItemMessages.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace Pilz.Net.Api.Messages; + +public static class GeneralItemMessages +{ + public class Item(T item) : ObjectMessage(item); + public class Items(List items) : ObjectsMessage(items); + + [EditorBrowsable(EditorBrowsableState.Never)] + public class ObjectMessage(T item) : ApiMessage + { + public T Item { get; } = item; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public class ObjectsMessage(List items) : ApiMessage + { + public List Items { get; } = items; + } +} diff --git a/Pilz.Net/Api/Server/BaseChildItemHandler.cs b/Pilz.Net/Api/Server/BaseChildItemHandler.cs new file mode 100644 index 0000000..16b0a94 --- /dev/null +++ b/Pilz.Net/Api/Server/BaseChildItemHandler.cs @@ -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(IApiServer server) + : BaseHandler(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 list; + + if (parent != 0 && server.Manager.Find(parent, out TParent? parentEntity)) + list = GetChilds(parentEntity).AsQueryable(); + else + list = server.Manager.Get(); + + 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 GetChilds(TParent parent); +} diff --git a/Pilz.Net/Api/Server/BaseHandler.cs b/Pilz.Net/Api/Server/BaseHandler.cs new file mode 100644 index 0000000..b5a8878 --- /dev/null +++ b/Pilz.Net/Api/Server/BaseHandler.cs @@ -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(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(id, true); + return ApiResult.Ok(); + } + + protected virtual TEntity CreateNewEntity() + { + return Activator.CreateInstance(); + } + + protected abstract ApiResult? UpdateEntity(TEntity entity, TUpdateMsg msg); + + protected abstract IDataObject ToClient(TEntity entity); +} diff --git a/Pilz.Net/Api/Server/BaseItemHandler.cs b/Pilz.Net/Api/Server/BaseItemHandler.cs new file mode 100644 index 0000000..cc49995 --- /dev/null +++ b/Pilz.Net/Api/Server/BaseItemHandler.cs @@ -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(IApiServer server) + : BaseHandler(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().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(); + } +} diff --git a/Pilz.Net/Data/DataManager.cs b/Pilz.Net/Data/DataManager.cs new file mode 100644 index 0000000..a2fbce6 --- /dev/null +++ b/Pilz.Net/Data/DataManager.cs @@ -0,0 +1,149 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Pilz.Net.Data; + +public abstract class DataManager : IDataManager +{ + protected abstract void UpdateEntity(T obj) where T : IDataObject; + protected abstract IQueryable GetEntitySet() where T : IDataObject; + protected abstract T? FindEntity(int id) where T : IDataObject; + protected abstract void RemoveEntity(T obj) where T : IDataObject; + protected abstract void SaveChanges(); + + public virtual IQueryable Get() where T : IDataObject + { + return GetEntitySet(); + } + + public virtual T FindOrNew(int? id) where T : IDataObject + { + if (Find(id) is not T obj) + obj = Activator.CreateInstance(); + return obj; + } + + public virtual T FindOrNew(int id) where T : IDataObject + { + if (Find(id) is not T obj) + obj = Activator.CreateInstance(); + return obj; + } + + public virtual T? FindOrNull(int? id) where T : IDataObject + { + if (id == null || id == 0) + return default; + return Find(id); + } + + public virtual T? FindOrNull(int id) where T : IDataObject + { + if (id == 0) + return default; + return Find(id); + } + + public virtual T? Find(int? id) where T : IDataObject + { + if (id == null) + return default; + return Find(id.Value); + } + + public virtual T? Find(int id) where T : IDataObject + { + return FindEntity(id); + } + + public virtual bool FindOrNew(int? id, [NotNullWhen(true)] out T? obj) where T : IDataObject + { + if (id == null || id == 0) + { + obj = Activator.CreateInstance(); + return obj != null; + } + return Find(id.Value, out obj); + } + + public virtual bool FindOrNew(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject + { + if (id == 0) + { + obj = Activator.CreateInstance(); + return obj != null; + } + return Find(id, out obj); + } + + public virtual bool FindOrNull(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(int id, out T? obj) where T : IDataObject + { + if (id == 0) + { + obj = default; + return true; + } + return Find(id, out obj); + } + + public virtual bool Find(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject + { + if (FindEntity(id) is not T t) + { + obj = default; + return false; + } + obj = t; + return true; + } + + public virtual void Delete(int id) where T : IDataObject + { + Delete(id, false); + } + + public virtual void Delete(int id, bool save) where T : IDataObject + { + if (Find(id, out T? obj)) + Delete(obj, save); + } + + public virtual void Delete(T? obj) where T : IDataObject + { + Delete(obj, false); + } + + public virtual void Delete(T? obj, bool save) where T : IDataObject + { + if (obj != null) + { + RemoveEntity(obj); + if (save) Save(); + } + } + + public virtual void Save(T obj) where T : IDataObject + { + Save(obj, false); + } + + public virtual void Save(T obj, bool save) where T : IDataObject + { + UpdateEntity(obj); + if (save) Save(); + } + + public virtual void Save() + { + SaveChanges(); + } +} diff --git a/Pilz.Net/Data/DataManagerEventArgs.cs b/Pilz.Net/Data/DataManagerEventArgs.cs new file mode 100644 index 0000000..85b0978 --- /dev/null +++ b/Pilz.Net/Data/DataManagerEventArgs.cs @@ -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); \ No newline at end of file diff --git a/Pilz.Net/Data/IDataManager.cs b/Pilz.Net/Data/IDataManager.cs new file mode 100644 index 0000000..6648694 --- /dev/null +++ b/Pilz.Net/Data/IDataManager.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Pilz.Net.Data; + +public interface IDataManager +{ + void Delete(int id) where T : IDataObject; + void Delete(int id, bool save) where T : IDataObject; + void Delete(T? obj) where T : IDataObject; + void Delete(T? obj, bool save) where T : IDataObject; + T? Find(int id) where T : IDataObject; + bool Find(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject; + T? Find(int? id) where T : IDataObject; + T FindOrNew(int id) where T : IDataObject; + bool FindOrNew(int id, [NotNullWhen(true)] out T? obj) where T : IDataObject; + T FindOrNew(int? id) where T : IDataObject; + bool FindOrNew(int? id, [NotNullWhen(true)] out T? obj) where T : IDataObject; + T? FindOrNull(int id) where T : IDataObject; + bool FindOrNull(int id, out T? obj) where T : IDataObject; + T? FindOrNull(int? id) where T : IDataObject; + bool FindOrNull(int? id, out T? obj) where T : IDataObject; + IQueryable Get() where T : IDataObject; + void Save(); + void Save(T obj) where T : IDataObject; + void Save(T obj, bool save) where T : IDataObject; +} \ No newline at end of file diff --git a/Pilz.Net/Data/IDataObject.cs b/Pilz.Net/Data/IDataObject.cs new file mode 100644 index 0000000..562e335 --- /dev/null +++ b/Pilz.Net/Data/IDataObject.cs @@ -0,0 +1,6 @@ +namespace Pilz.Net.Data; + +public interface IDataObject +{ + public int Id { get; } +} diff --git a/Pilz.Net/Extensions/ApiMessageExtensions.cs b/Pilz.Net/Extensions/ApiMessageExtensions.cs new file mode 100644 index 0000000..7d84dab --- /dev/null +++ b/Pilz.Net/Extensions/ApiMessageExtensions.cs @@ -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(this T? @this) + { + if (@this == null) + return ApiResult.NotFound(); + return ApiResult.Ok(@this.ToItemMsg()); + } + + public static ApiResult ToItemsResult(this IEnumerable? @this) + { + if (@this == null) + return ApiResult.NotFound(); + return ApiResult.Ok(@this.ToItemsMsg()); + } + + public static GeneralItemMessages.Item ToItemMsg(this T @this) + { + return new GeneralItemMessages.Item(@this); + } + + public static GeneralItemMessages.Items ToItemsMsg(this IEnumerable @this) + { + return new GeneralItemMessages.Items([.. @this]); + } + + public static GeneralItemMessages.Items ToItemsMsg(this T[] @this) + { + return new GeneralItemMessages.Items([.. @this]); + } +}