diff --git a/Directory.Packages.props b/Directory.Packages.props
index e95a987..94e2d6a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,9 +5,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/Tribufu.sln b/Tribufu.sln
index b3ff43a..fd461d5 100644
--- a/Tribufu.sln
+++ b/Tribufu.sln
@@ -7,6 +7,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Generated", "src\Tr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Example", "src\Tribufu.Example\Tribufu.Example.csproj", "{D6392A29-E2DC-4050-B4C1-B279DD2D226D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Logging", "src\Tribufu.Logging\Tribufu.Logging.csproj", "{CFD80847-9B98-4991-BADF-8714E7D8D81C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Configuration", "src\Tribufu.Configuration\Tribufu.Configuration.csproj", "{C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Runtime", "src\Tribufu.Runtime\Tribufu.Runtime.csproj", "{26EEB407-733C-4383-9211-B083CD5F593B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Database", "src\Tribufu.Database\Tribufu.Database.csproj", "{E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.Serialization", "src\Tribufu.Serialization\Tribufu.Serialization.csproj", "{D6DAE078-2F80-49DD-97A3-B1223FE04F91}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tribufu.ComponentModel", "src\Tribufu.ComponentModel\Tribufu.ComponentModel.csproj", "{7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +65,78 @@ Global
{D6392A29-E2DC-4050-B4C1-B279DD2D226D}.Release|x64.Build.0 = Release|Any CPU
{D6392A29-E2DC-4050-B4C1-B279DD2D226D}.Release|x86.ActiveCfg = Release|Any CPU
{D6392A29-E2DC-4050-B4C1-B279DD2D226D}.Release|x86.Build.0 = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|x64.Build.0 = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Debug|x86.Build.0 = Debug|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|x64.ActiveCfg = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|x64.Build.0 = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|x86.ActiveCfg = Release|Any CPU
+ {CFD80847-9B98-4991-BADF-8714E7D8D81C}.Release|x86.Build.0 = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|x64.Build.0 = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Debug|x86.Build.0 = Debug|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|x64.ActiveCfg = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|x64.Build.0 = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|x86.ActiveCfg = Release|Any CPU
+ {C0A841C8-9FC5-4AC0-B9AD-6BBFCEDCBE5F}.Release|x86.Build.0 = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|x64.Build.0 = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Debug|x86.Build.0 = Debug|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|x64.ActiveCfg = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|x64.Build.0 = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|x86.ActiveCfg = Release|Any CPU
+ {26EEB407-733C-4383-9211-B083CD5F593B}.Release|x86.Build.0 = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|x64.Build.0 = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Debug|x86.Build.0 = Debug|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|x64.ActiveCfg = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|x64.Build.0 = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|x86.ActiveCfg = Release|Any CPU
+ {E7F9A76F-C087-410B-B4B5-A928A6CDC2BA}.Release|x86.Build.0 = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|x64.Build.0 = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Debug|x86.Build.0 = Debug|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|x64.ActiveCfg = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|x64.Build.0 = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|x86.ActiveCfg = Release|Any CPU
+ {D6DAE078-2F80-49DD-97A3-B1223FE04F91}.Release|x86.Build.0 = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|x64.Build.0 = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Debug|x86.Build.0 = Debug|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|x64.ActiveCfg = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|x64.Build.0 = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|x86.ActiveCfg = Release|Any CPU
+ {7CB04FFD-8F4B-4B40-BB4B-2BAA19D783E1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/scripts/package.ps1 b/scripts/package.ps1
new file mode 100644
index 0000000..7408aae
--- /dev/null
+++ b/scripts/package.ps1
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+
+dotnet clean
+dotnet build -c Release
+dotnet pack
diff --git a/scripts/package.sh b/scripts/package.sh
deleted file mode 100644
index 58f8eb2..0000000
--- a/scripts/package.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env sh
-
-dotnet pack
diff --git a/src/Tribufu.ComponentModel/EnumMemberConverter.cs b/src/Tribufu.ComponentModel/EnumMemberConverter.cs
new file mode 100644
index 0000000..38d62e3
--- /dev/null
+++ b/src/Tribufu.ComponentModel/EnumMemberConverter.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Runtime.Serialization;
+
+namespace Tribufu.ComponentModel
+{
+ public class EnumMemberConverter : EnumConverter
+ {
+ public EnumMemberConverter(Type type) : base(type) { }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ var type = typeof(T);
+
+ foreach (var field in type.GetFields())
+ {
+ if (Attribute.GetCustomAttribute(field, typeof(EnumMemberAttribute)) is EnumMemberAttribute attribute && value is string enumValue && attribute.Value == enumValue)
+ {
+ return field.GetValue(null);
+ }
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+ }
+}
diff --git a/src/Tribufu.ComponentModel/README.md b/src/Tribufu.ComponentModel/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.ComponentModel/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.ComponentModel/Tribufu.ComponentModel.csproj b/src/Tribufu.ComponentModel/Tribufu.ComponentModel.csproj
new file mode 100644
index 0000000..4bcae0c
--- /dev/null
+++ b/src/Tribufu.ComponentModel/Tribufu.ComponentModel.csproj
@@ -0,0 +1,16 @@
+
+
+ Tribufu.ComponentModel
+ Tribufu ComponentModel Extensions
+ README.md
+
+
+ Properties
+ true
+ Library
+ netstandard2.0;net45;net5.0
+
+
+
+
+
diff --git a/src/Tribufu.Configuration/ConfigurationManager.cs b/src/Tribufu.Configuration/ConfigurationManager.cs
new file mode 100644
index 0000000..009b6e7
--- /dev/null
+++ b/src/Tribufu.Configuration/ConfigurationManager.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Microsoft.Extensions.Configuration;
+using System.IO;
+using Tomlyn.Extensions.Configuration;
+using Tribufu.Logging;
+using Tribufu.Runtime;
+
+namespace Tribufu.Configuration
+{
+ public static class ConfigurationManager
+ {
+ public static IConfiguration Configuration { get; private set; }
+
+ public static IConfiguration Load(string[] fileNames)
+ {
+ var configDirectory = ApplicationContext.GetConfigDirectory();
+ var configurationBuilder = new ConfigurationBuilder();
+ configurationBuilder.AddEnvironmentVariables();
+
+ foreach (var fileName in fileNames)
+ {
+ var fullPath = Path.Combine(configDirectory, fileName);
+ if (!File.Exists(fullPath))
+ {
+ Logger.Debug($"Config file '{fullPath}' not found, skipping.");
+ continue;
+ }
+
+ var ext = Path.GetExtension(fullPath).ToLowerInvariant();
+ switch (ext)
+ {
+ case ".ini":
+ configurationBuilder.AddIniFile(fullPath, true, false);
+ break;
+ case ".json":
+ configurationBuilder.AddJsonFile(fullPath, true, false);
+ break;
+ case ".toml":
+ configurationBuilder.AddTomlFile(fullPath, true, false);
+ break;
+ default:
+ Logger.Warn($"Unsupported config file extension: {ext}");
+ break;
+ }
+ }
+
+ Configuration = configurationBuilder.Build();
+ return Configuration;
+ }
+ }
+}
diff --git a/src/Tribufu.Configuration/README.md b/src/Tribufu.Configuration/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.Configuration/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.Configuration/Tribufu.Configuration.csproj b/src/Tribufu.Configuration/Tribufu.Configuration.csproj
new file mode 100644
index 0000000..475a02c
--- /dev/null
+++ b/src/Tribufu.Configuration/Tribufu.Configuration.csproj
@@ -0,0 +1,30 @@
+
+
+ Tribufu.Configuration
+ Tribufu Configuration Extensions
+ README.md
+
+
+ Properties
+ true
+ Library
+ netstandard2.0;net6.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tribufu.Database/DatabaseConfiguration.cs b/src/Tribufu.Database/DatabaseConfiguration.cs
new file mode 100644
index 0000000..915f1d2
--- /dev/null
+++ b/src/Tribufu.Database/DatabaseConfiguration.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Microsoft.Extensions.Configuration;
+using System;
+
+namespace Tribufu.Database
+{
+ public class DatabaseConfiguration
+ {
+ public DatabaseDriver Driver { get; set; }
+
+ public string? Version { get; set; }
+
+ public string? Host { get; set; }
+
+ public string? Port { get; set; }
+
+ public string? User { get; set; }
+
+ public string? Password { get; set; }
+
+ public string? Schema { get; set; }
+
+ ///
+ /// Loads the from the "database" section or from root-level keys prefixed with "database_".
+ ///
+ /// The configuration source.
+ /// The populated instance.
+ public static DatabaseConfiguration Load(IConfiguration configuration)
+ {
+ var section = configuration.GetSection("database");
+ var useRootFallback = !section.Exists();
+
+ string? GetConfig(string key) => useRootFallback ? configuration[$"database_{key}"] : section[key];
+
+ var driverString = GetConfig("driver") ?? throw new Exception("Missing database driver");
+ if (!Enum.TryParse(driverString, true, out var driver))
+ {
+ throw new Exception($"Unsupported database driver: {driverString}");
+ }
+
+ return new DatabaseConfiguration
+ {
+ Driver = driver,
+ Version = GetConfig("version"),
+ Host = GetConfig("host"),
+ Port = GetConfig("port"),
+ User = GetConfig("user"),
+ Password = GetConfig("password"),
+ Schema = GetConfig("schema")
+ };
+ }
+ }
+}
diff --git a/src/Tribufu.Database/DatabaseConstants.cs b/src/Tribufu.Database/DatabaseConstants.cs
new file mode 100644
index 0000000..bfff4b2
--- /dev/null
+++ b/src/Tribufu.Database/DatabaseConstants.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+namespace Tribufu.Database
+{
+ public static class DatabaseConstants
+ {
+ public const uint DEFAULT_PAGINATION = 20;
+
+ public const uint MAX_PAGINATION = 100;
+ }
+}
diff --git a/src/Tribufu.Database/DatabaseDriver.cs b/src/Tribufu.Database/DatabaseDriver.cs
new file mode 100644
index 0000000..8c3f8f3
--- /dev/null
+++ b/src/Tribufu.Database/DatabaseDriver.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+namespace Tribufu.Database
+{
+ public enum DatabaseDriver : byte
+ {
+ MySql = 0,
+
+ Postgres = 1,
+
+ SqlServer = 2,
+
+ Oracle = 3,
+
+ Firebird = 4,
+
+ Sqlite = 5,
+
+ MongoDb = 6,
+ }
+}
diff --git a/src/Tribufu.Database/README.md b/src/Tribufu.Database/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.Database/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.Database/Repositories/IRepository.cs b/src/Tribufu.Database/Repositories/IRepository.cs
new file mode 100644
index 0000000..97f2750
--- /dev/null
+++ b/src/Tribufu.Database/Repositories/IRepository.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Tribufu.Database.Repositories
+{
+ public interface IRepository where T : class
+ {
+ IList GetAll();
+
+ Task> GetAllAsync();
+
+ IList GetPage(uint page, uint limit);
+
+ Task> GetPageAsync(uint page, uint limit);
+
+ T? GetOne(K key);
+
+ Task GetOneAsync(K key);
+
+ T? Create(T entity);
+
+ Task CreateAsync(T entity);
+
+ T? Update(T entity);
+
+ Task UpdateAsync(T entity);
+
+ void Delete(K key);
+
+ Task DeleteAsync(K key);
+
+ void Delete(T entity);
+
+ Task DeleteAsync(T entity);
+ }
+}
diff --git a/src/Tribufu.Database/Repositories/Repository.cs b/src/Tribufu.Database/Repositories/Repository.cs
new file mode 100644
index 0000000..a929382
--- /dev/null
+++ b/src/Tribufu.Database/Repositories/Repository.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Tribufu.Database.Repositories
+{
+ public class Repository : IRepository where C : DbContext where T : class
+ {
+ protected readonly C _context;
+
+ protected readonly DbSet _dbSet;
+
+ public Repository(C context)
+ {
+ _context = context ?? throw new ArgumentNullException(nameof(context));
+ _dbSet = context.Set();
+ }
+
+ public virtual IList GetAll()
+ {
+ return [.. _dbSet];
+ }
+
+ public virtual async Task> GetAllAsync()
+ {
+ return await _dbSet.ToListAsync();
+ }
+
+ public virtual IList GetPage(uint page, uint limit)
+ {
+ return _dbSet.Skip((int)((page < 1 ? 0 : page - 1) * limit)).Take((int)limit).ToList();
+ }
+
+ public virtual async Task> GetPageAsync(uint page, uint limit)
+ {
+ return await _dbSet.Skip((int)((page < 1 ? 0 : page - 1) * limit)).Take((int)limit).ToListAsync();
+ }
+
+ public virtual T? GetOne(K key)
+ {
+ return _dbSet.Find(key);
+ }
+
+ public virtual async Task GetOneAsync(K key)
+ {
+ return await _dbSet.FindAsync(key);
+ }
+
+ public virtual T? Create(T entity)
+ {
+ _dbSet.Add(entity);
+
+ var result = _context.SaveChanges();
+ return result > 0 ? entity : null;
+ }
+
+ public virtual async Task CreateAsync(T entity)
+ {
+ await _dbSet.AddAsync(entity);
+ var result = await _context.SaveChangesAsync();
+ return result > 0 ? entity : null;
+ }
+
+ public virtual T? Update(T entity)
+ {
+ _dbSet.Update(entity);
+ var result = _context.SaveChanges();
+ return result > 0 ? entity : null;
+ }
+
+ public virtual async Task UpdateAsync(T entity)
+ {
+ _dbSet.Update(entity);
+ var result = await _context.SaveChangesAsync();
+ return result > 0 ? entity : null;
+ }
+
+ public virtual void Delete(K key)
+ {
+ var entity = _dbSet.Find(key);
+ if (entity != null)
+ {
+ Delete(entity);
+ }
+ }
+
+ public virtual async Task DeleteAsync(K key)
+ {
+ var entity = await _dbSet.FindAsync(key);
+ if (entity != null)
+ {
+ await DeleteAsync(entity);
+ }
+ }
+
+ public virtual void Delete(T entity)
+ {
+ _dbSet.Remove(entity);
+ _context.SaveChanges();
+ }
+
+ public virtual async Task DeleteAsync(T entity)
+ {
+ _dbSet.Remove(entity);
+ await _context.SaveChangesAsync();
+ }
+ }
+}
diff --git a/src/Tribufu.Database/Tribufu.Database.csproj b/src/Tribufu.Database/Tribufu.Database.csproj
new file mode 100644
index 0000000..dd69953
--- /dev/null
+++ b/src/Tribufu.Database/Tribufu.Database.csproj
@@ -0,0 +1,25 @@
+
+
+ Tribufu.Database
+ Tribufu Database Extensions
+ README.md
+
+
+ Properties
+ true
+ enable
+ Library
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tribufu.Example/Program.cs b/src/Tribufu.Example/Program.cs
index 5462790..c6f9e1d 100644
--- a/src/Tribufu.Example/Program.cs
+++ b/src/Tribufu.Example/Program.cs
@@ -1,8 +1,9 @@
// Copyright (c) Tribufu. All Rights Reserved.
-// SPDX-License-Identifier: UNLICENSED
+// SPDX-License-Identifier: MIT
using dotenv.net;
using Tribufu.Generated.Client;
+using Tribufu.Logging;
namespace Tribufu.Test
{
@@ -10,21 +11,23 @@ namespace Tribufu.Test
{
public static async Task Main(string[] args)
{
+ Logger.Initialize(LogLevel.All);
+
DotEnv.Load(new DotEnvOptions(ignoreExceptions: true, envFilePaths: [".env", "../../.env"]));
var apiKey = Environment.GetEnvironmentVariable("TRIBUFU_API_KEY");
var tribufu = new TribufuApi(apiKey ?? "");
- Console.WriteLine(TribufuApi.GetVersion());
+ Logger.Debug(TribufuApi.GetVersion());
try
{
var result = await tribufu.GetUserInfoAsync();
- Console.WriteLine(result);
+ Logger.Debug(result.ToString());
}
catch (ApiException e)
{
- Console.WriteLine(e.Message);
+ Logger.Debug(e.Message);
}
}
}
diff --git a/src/Tribufu.Example/Tribufu.Example.csproj b/src/Tribufu.Example/Tribufu.Example.csproj
index a6fbc73..c5cbd10 100644
--- a/src/Tribufu.Example/Tribufu.Example.csproj
+++ b/src/Tribufu.Example/Tribufu.Example.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/Tribufu.Logging/LogLevel.cs b/src/Tribufu.Logging/LogLevel.cs
new file mode 100644
index 0000000..f13e62c
--- /dev/null
+++ b/src/Tribufu.Logging/LogLevel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+namespace Tribufu.Logging
+{
+ public enum LogLevel : byte
+ {
+ Off = 0,
+
+ Trace = 1,
+
+ Debug = 2,
+
+ Info = 3,
+
+ Warn = 4,
+
+ Error = 5,
+
+ All = 6,
+ }
+}
diff --git a/src/Tribufu.Logging/Logger.cs b/src/Tribufu.Logging/Logger.cs
new file mode 100644
index 0000000..38e01f6
--- /dev/null
+++ b/src/Tribufu.Logging/Logger.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using System;
+
+namespace Tribufu.Logging
+{
+ public static class Logger
+ {
+ private static LogLevel _level = LogLevel.Off;
+
+ public static void Initialize(LogLevel level = LogLevel.Off)
+ {
+ _level = level;
+ }
+
+ public static void Info(string message)
+ {
+ Log(LogLevel.Info, message, ConsoleColor.Green);
+ }
+
+ public static void Warn(string message)
+ {
+ Log(LogLevel.Warn, message, ConsoleColor.Yellow);
+ }
+
+ public static void Error(string message)
+ {
+ Log(LogLevel.Error, message, ConsoleColor.Red);
+ }
+
+ public static void Debug(string message)
+ {
+ Log(LogLevel.Debug, message, ConsoleColor.White);
+ }
+
+ public static void Trace(string message)
+ {
+ Log(LogLevel.Trace, message, ConsoleColor.Gray);
+ }
+
+ private static void Log(LogLevel level, string message, ConsoleColor color)
+ {
+ if (_level == LogLevel.Off)
+ {
+ return;
+ }
+
+ if (_level == LogLevel.All || level >= _level)
+ {
+ var defaultColor = Console.ForegroundColor;
+ Console.ForegroundColor = color;
+ var timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
+ Console.WriteLine($"[{timestamp}] [{level.ToString().ToUpper()}]: {message}");
+ Console.ForegroundColor = defaultColor;
+ }
+ }
+ }
+}
diff --git a/src/Tribufu.Logging/README.md b/src/Tribufu.Logging/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.Logging/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.Logging/Tribufu.Logging.csproj b/src/Tribufu.Logging/Tribufu.Logging.csproj
new file mode 100644
index 0000000..516c7f9
--- /dev/null
+++ b/src/Tribufu.Logging/Tribufu.Logging.csproj
@@ -0,0 +1,18 @@
+
+
+ Tribufu.Logging
+ Tribufu Logging Extensions
+ README.md
+
+
+ Properties
+ true
+ Library
+ netstandard2.0;net45;net5.0
+
+
+
+
+
+
+
diff --git a/src/Tribufu.Runtime/ApplicationContext.cs b/src/Tribufu.Runtime/ApplicationContext.cs
new file mode 100644
index 0000000..ef26266
--- /dev/null
+++ b/src/Tribufu.Runtime/ApplicationContext.cs
@@ -0,0 +1,183 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Tribufu.Logging;
+
+namespace Tribufu.Runtime
+{
+ ///
+ /// Provides standardized access to important application directories, such as config, saved data, logs, and platform-specific binaries.
+ /// This is especially useful for abstracting file path logic across environments (development, production, etc).
+ ///
+ public static class ApplicationContext
+ {
+ ///
+ /// Gets the root base directory of the application.
+ ///
+ ///
+ /// - In development, this resolves to the root of the repository (five levels above bin/Debug or bin/Release).
+ /// - In production, it resolves to two levels above the binary location.
+ /// - It uses case-insensitive checks and runtime heuristics to improve accuracy.
+ ///
+ /// The absolute path to the base directory.
+ public static string GetBaseDirectory()
+ {
+ try
+ {
+ string baseDirectory;
+ string defaultBaseDirectory = AppContext.BaseDirectory;
+
+ bool isDevelopment = defaultBaseDirectory.ToLowerInvariant().Contains("debug");
+
+ if (isDevelopment)
+ {
+ // Go 5 levels up to simulate project root
+ baseDirectory = Path.Combine(defaultBaseDirectory, "..", "..", "..", "..", "..");
+ }
+ else
+ {
+ baseDirectory = Path.Combine(defaultBaseDirectory, "..", "..");
+ }
+
+ return Path.GetFullPath(baseDirectory);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn($"(ApplicationContext) Failed to resolve base directory: {ex.Message}");
+ return AppContext.BaseDirectory;
+ }
+ }
+
+ ///
+ /// Gets the path to the platform-specific binary directory.
+ ///
+ ///
+ /// The absolute path to bin/<runtime-identifier> if available,
+ /// otherwise falls back to bin/dotnet.
+ ///
+ public static string GetBinDirectory()
+ {
+ var binDirectory = Path.Combine(GetBaseDirectory(), "bin");
+
+#if NETSTANDARD
+ var runtimeIdentifier = GetRuntimeIdentifierLegacy();
+
+ if (!string.IsNullOrEmpty(runtimeIdentifier))
+ {
+ binDirectory = Path.Combine(binDirectory, runtimeIdentifier);
+ }
+ else
+ {
+ binDirectory = Path.Combine(binDirectory, "dotnet");
+ }
+#else
+ if (!string.IsNullOrEmpty(RuntimeInformation.RuntimeIdentifier))
+ {
+ binDirectory = Path.Combine(binDirectory, RuntimeInformation.RuntimeIdentifier);
+ }
+ else
+ {
+ binDirectory = Path.Combine(binDirectory, "dotnet");
+ }
+#endif
+
+ return binDirectory;
+ }
+
+ private static string GetRuntimeIdentifierLegacy()
+ {
+ string osPart;
+ PlatformID platform = Environment.OSVersion.Platform;
+
+ switch (platform)
+ {
+ case PlatformID.Win32NT:
+ osPart = "win";
+ break;
+ case PlatformID.Unix:
+ if (IsMacOS())
+ osPart = "osx";
+ else
+ osPart = "linux";
+ break;
+ case PlatformID.MacOSX:
+ osPart = "osx";
+ break;
+ default:
+ osPart = "unknown";
+ break;
+ }
+
+ var archPart = Environment.Is64BitProcess ? "x64" : "x86";
+ if (osPart == "unknown")
+ {
+ return null;
+ }
+
+ return $"{osPart}-{archPart}";
+ }
+
+ private static bool IsMacOS()
+ {
+ if (Environment.OSVersion.Platform == PlatformID.Unix)
+ {
+ Version version = Environment.OSVersion.Version;
+
+ if (version.Major >= 19)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Gets the path to the configuration directory.
+ ///
+ /// The absolute path to the config directory.
+ public static string GetConfigDirectory()
+ {
+ return Path.Combine(GetBaseDirectory(), "config");
+ }
+
+ ///
+ /// Gets the path to the assets directory.
+ ///
+ /// The absolute path to the assets directory.
+ public static string GetAssetsDirectory()
+ {
+ return Path.Combine(GetBaseDirectory(), "assets");
+ }
+
+ ///
+ /// Gets the path to the saved data directory.
+ ///
+ /// The absolute path to the saved directory.
+ public static string GetSavedDirectory()
+ {
+ return Path.Combine(GetBaseDirectory(), "saved");
+ }
+
+ ///
+ /// Gets the path to the cache directory inside saved.
+ ///
+ /// The absolute path to the saved/cache directory.
+ public static string GetCacheDirectory()
+ {
+ return Path.Combine(GetSavedDirectory(), "cache");
+ }
+
+ ///
+ /// Gets the path to the logs directory inside saved.
+ ///
+ /// The absolute path to the saved/logs directory.
+ public static string GetLogsDirectory()
+ {
+ return Path.Combine(GetSavedDirectory(), "logs");
+ }
+ }
+}
diff --git a/src/Tribufu.Runtime/README.md b/src/Tribufu.Runtime/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.Runtime/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.Runtime/Tribufu.Runtime.csproj b/src/Tribufu.Runtime/Tribufu.Runtime.csproj
new file mode 100644
index 0000000..0d16370
--- /dev/null
+++ b/src/Tribufu.Runtime/Tribufu.Runtime.csproj
@@ -0,0 +1,19 @@
+
+
+ Tribufu.Runtime
+ Tribufu Runtime Extensions
+ README.md
+
+
+ Properties
+ true
+ Library
+ netstandard2.0;net5.0
+
+
+
+
+
+
+
+
diff --git a/src/Tribufu.Serialization/DecimalNullableStringConverter.cs b/src/Tribufu.Serialization/DecimalNullableStringConverter.cs
new file mode 100644
index 0000000..fee4870
--- /dev/null
+++ b/src/Tribufu.Serialization/DecimalNullableStringConverter.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Newtonsoft.Json;
+using System;
+using System.Globalization;
+
+namespace Tribufu.Serialization
+{
+ public class DecimalNullableStringConverter : JsonConverter
+ {
+ public override decimal? ReadJson(JsonReader reader, Type objectType, decimal? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType == JsonToken.String || reader.TokenType == JsonToken.Integer)
+ {
+ string value = reader.Value?.ToString();
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ return decimal.Parse(value);
+ }
+
+ throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing decimal?.");
+ }
+
+ public override void WriteJson(JsonWriter writer, decimal? value, JsonSerializer serializer)
+ {
+ if (value.HasValue)
+ {
+ writer.WriteValue(value.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ else
+ {
+ writer.WriteNull();
+ }
+ }
+ }
+}
diff --git a/src/Tribufu.Serialization/DecimalStringConverter.cs b/src/Tribufu.Serialization/DecimalStringConverter.cs
new file mode 100644
index 0000000..2794fa3
--- /dev/null
+++ b/src/Tribufu.Serialization/DecimalStringConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Newtonsoft.Json;
+using System;
+using System.Globalization;
+
+namespace Tribufu.Serialization
+{
+ public class DecimalStringConverter : JsonConverter
+ {
+ public override decimal ReadJson(JsonReader reader, Type objectType, decimal existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.String && decimal.TryParse(reader.Value?.ToString(), out var result))
+ {
+ return result;
+ }
+
+ if (reader.TokenType == JsonToken.Integer)
+ {
+ return Convert.ToUInt64(reader.Value);
+ }
+
+ throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing decimal.");
+ }
+
+ public override void WriteJson(JsonWriter writer, decimal value, JsonSerializer serializer)
+ {
+ writer.WriteValue(value.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+}
diff --git a/src/Tribufu.Serialization/README.md b/src/Tribufu.Serialization/README.md
new file mode 100644
index 0000000..54c1ab5
--- /dev/null
+++ b/src/Tribufu.Serialization/README.md
@@ -0,0 +1 @@
+# Tribufu
diff --git a/src/Tribufu.Serialization/Tribufu.Serialization.csproj b/src/Tribufu.Serialization/Tribufu.Serialization.csproj
new file mode 100644
index 0000000..e0b4fc8
--- /dev/null
+++ b/src/Tribufu.Serialization/Tribufu.Serialization.csproj
@@ -0,0 +1,19 @@
+
+
+ Tribufu.Serialization
+ Tribufu Serialization Extensions
+ README.md
+
+
+ Properties
+ true
+ Library
+ netstandard2.0;net45;net5.0
+
+
+
+
+
+
+
+
diff --git a/src/Tribufu.Serialization/ULongNullableStringConverter.cs b/src/Tribufu.Serialization/ULongNullableStringConverter.cs
new file mode 100644
index 0000000..0beb8e4
--- /dev/null
+++ b/src/Tribufu.Serialization/ULongNullableStringConverter.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Newtonsoft.Json;
+using System;
+
+namespace Tribufu.Serialization
+{
+ public class ULongNullableStringConverter : JsonConverter
+ {
+ public override ulong? ReadJson(JsonReader reader, Type objectType, ulong? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType == JsonToken.String || reader.TokenType == JsonToken.Integer)
+ {
+ string value = reader.Value?.ToString();
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ return ulong.Parse(value);
+ }
+
+ throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing ulong?.");
+ }
+
+ public override void WriteJson(JsonWriter writer, ulong? value, JsonSerializer serializer)
+ {
+ if (value.HasValue)
+ {
+ writer.WriteValue(value.Value.ToString());
+ }
+ else
+ {
+ writer.WriteNull();
+ }
+ }
+ }
+}
diff --git a/src/Tribufu.Serialization/ULongStringConverter.cs b/src/Tribufu.Serialization/ULongStringConverter.cs
new file mode 100644
index 0000000..5cb6f33
--- /dev/null
+++ b/src/Tribufu.Serialization/ULongStringConverter.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Tribufu. All Rights Reserved.
+// SPDX-License-Identifier: MIT
+
+using Newtonsoft.Json;
+using System;
+
+namespace Tribufu.Serialization
+{
+ public class ULongStringConverter : JsonConverter
+ {
+ public override ulong ReadJson(JsonReader reader, Type objectType, ulong existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.String && ulong.TryParse(reader.Value?.ToString(), out var result))
+ {
+ return result;
+ }
+
+ if (reader.TokenType == JsonToken.Integer)
+ {
+ return Convert.ToUInt64(reader.Value);
+ }
+
+ throw new JsonSerializationException($"Unexpected token {reader.TokenType} when parsing ulong.");
+ }
+
+ public override void WriteJson(JsonWriter writer, ulong value, JsonSerializer serializer)
+ {
+ writer.WriteValue(value.ToString());
+ }
+ }
+}