This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
namespace Prefab.Base.Catalog.Attributes;
public static class AttributeDefinitionRules
{
public const int NameMaxLength = 256;
public const int UnitMaxLength = 32;
}
public static class ProductAttributeValueRules
{
public const int ValueMaxLength = 1024;
public const int UnitCodeMaxLength = 32;
public const int EnumCodeMaxLength = 64;
}

View File

@@ -0,0 +1,11 @@
namespace Prefab.Base.Catalog.Categories;
public class CategoryRules
{
public const int NameMaxLength = 100;
public const int DescriptionMaxLength = 500;
public const int SlugMaxLength = 128;
public const int HeroImageUrlMaxLength = 512;
public const int IconMaxLength = 128;
}

View File

@@ -0,0 +1,14 @@
namespace Prefab.Base.Catalog.Options;
public static class OptionDefinitionRules
{
public const int CodeMaxLength = 64;
public const int NameMaxLength = 256;
public const int UnitMaxLength = 32;
}
public static class OptionValueRules
{
public const int CodeMaxLength = 64;
public const int LabelMaxLength = 256;
}

View File

@@ -0,0 +1,10 @@
namespace Prefab.Base.Catalog.Products;
public static class ProductRules
{
public const int NameMaxLength = 256;
public const int SlugMaxLength = 256;
public const int SkuMaxLength = 64;
public const int DescriptionMaxLength = 2048;
public const string SlugPattern = "^[a-z0-9]+(?:-[a-z0-9]+)*$";
}

18
Prefab.Base/Enums.cs Normal file
View File

@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Prefab.Base;
/// <summary>
/// Represents the supported default cultures.
/// </summary>
public enum Culture
{
[Display(Order = 1, Name = "en-US")]
UnitedStates,
[Display(Order = 2, Name = "en-GB")]
UnitedKingdom,
[Display(Order = 2, Name = "ja-JP")]
Japan
}

View File

@@ -0,0 +1,6 @@
namespace Prefab.Base;
/// <summary>
/// Represents a change in a custom attribute.
/// </summary>
public record GenericAttributeChange(string Key, object? OldValue, object? NewValue);

View File

@@ -0,0 +1,88 @@
using System.Collections.ObjectModel;
namespace Prefab.Base;
/// <summary>
/// Provides a customizable attribute bag with change tracking for Prefab domain objects.
/// </summary>
public class GenericAttributes : ICanBeCustomized
{
private readonly Dictionary<string, object> _attributes = new();
private readonly List<GenericAttributeChange> _changes = new();
/// <inheritdoc />
IReadOnlyDictionary<string, object> IHasGenericAttributes.GenericAttributes => new ReadOnlyDictionary<string, object>(_attributes);
/// <summary>
/// Gets the ordered list of attribute mutations recorded since the last call to <see cref="ClearChanges"/>.
/// </summary>
public IReadOnlyList<GenericAttributeChange> Changes => _changes.AsReadOnly();
/// <summary>
/// Provides an extension point for enforcing key/value requirements before persisting the attribute.
/// </summary>
/// <param name="key">The attribute key being modified.</param>
/// <param name="value">The proposed attribute value.</param>
protected virtual void ValidateAttribute(string key, object value)
{
// Default: no validation. Override in derived classes for custom logic.
}
/// <summary>
/// Sets or replaces an attribute value and records the mutation.
/// </summary>
/// <param name="key">The attribute key to set.</param>
/// <param name="value">The attribute value.</param>
public void SetAttribute(string key, object value)
{
_attributes.TryGetValue(key, out var oldValue);
ValidateAttribute(key, value);
_attributes[key] = value;
_changes.Add(new GenericAttributeChange(key, oldValue, value));
}
/// <summary>
/// Removes an attribute if present and records the removal.
/// </summary>
/// <param name="key">The attribute key to remove.</param>
/// <returns><c>true</c> when the key existed and was removed; otherwise <c>false</c>.</returns>
public bool RemoveAttribute(string key)
{
if (_attributes.TryGetValue(key, out var oldValue) && _attributes.Remove(key))
{
_changes.Add(new GenericAttributeChange(key, oldValue, null));
return true;
}
return false;
}
/// <summary>
/// Attempts to read an attribute value and cast it to the requested type.
/// </summary>
/// <param name="key">The attribute key to read.</param>
/// <param name="value">Receives the typed attribute value when successful.</param>
/// <typeparam name="T">The expected attribute type.</typeparam>
/// <returns><c>true</c> when the attribute exists and matches the requested type.</returns>
public bool TryGetAttribute<T>(string key, out T value)
{
if (_attributes.TryGetValue(key, out var obj) && obj is T t)
{
value = t;
return true;
}
value = default!;
return false;
}
/// <summary>
/// Creates an immutable snapshot copy of the current attributes.
/// </summary>
public IReadOnlyDictionary<string, object> Snapshot() => new ReadOnlyDictionary<string, object>(new Dictionary<string, object>(_attributes));
/// <summary>
/// Clears all recorded attribute changes while keeping the current attribute values intact.
/// </summary>
public void ClearChanges() => _changes.Clear();
}

View File

@@ -0,0 +1,30 @@
namespace Prefab.Base;
/// <summary>
/// Contract for models that expose a customizable attribute bag to module developers.
/// </summary>
public interface ICanBeCustomized : IHasGenericAttributes
{
/// <summary>
/// Sets or updates a custom attribute value.
/// </summary>
/// <param name="key">The attribute name.</param>
/// <param name="value">The value to store.</param>
void SetAttribute(string key, object value);
/// <summary>
/// Removes a custom attribute by key.
/// </summary>
/// <param name="key">The attribute to remove.</param>
/// <returns><c>true</c> when the key existed and was removed.</returns>
bool RemoveAttribute(string key);
/// <summary>
/// Attempts to read an attribute value.
/// </summary>
/// <param name="key">The attribute to inspect.</param>
/// <param name="value">Receives the typed attribute value when present.</param>
/// <typeparam name="T">The expected attribute type.</typeparam>
/// <returns><c>true</c> when the attribute exists and matches the requested type.</returns>
bool TryGetAttribute<T>(string key, out T value);
}

9
Prefab.Base/IEvent.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace Prefab.Base;
/// <summary>
/// Marker interface for events.
/// </summary>
public interface IEvent
{
}

View File

@@ -0,0 +1,12 @@
namespace Prefab.Base;
/// <summary>
/// Readonly view of the generic attributes bag.
/// </summary>
public interface IHasGenericAttributes
{
/// <summary>
/// Gets the read-only dictionary of custom attributes.
/// </summary>
IReadOnlyDictionary<string, object> GenericAttributes { get; }
}

12
Prefab.Base/ISortOrder.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Prefab.Base;
/// <summary>
/// Defines a contract for entities that expose an explicit sort position.
/// </summary>
public interface ISortOrder
{
/// <summary>
/// Gets or sets the relative sort order for the entity.
/// </summary>
int SortOrder { get; set; }
}

View File

@@ -0,0 +1,53 @@
namespace Prefab.Base;
/// <summary>
/// Represents a lookup entity that can be used to represent a PrefabEnum in a database table.
/// </summary>
/// <typeparam name="TEnum">The type that will represent the entity's backed <see cref="PrefabEnum" /> type.</typeparam>
public abstract class LookupEntity<TEnum> : ISortOrder where TEnum : PrefabEnum<TEnum>
{
/// <summary>
/// The underlying integer value of the enum.
/// </summary>
public int Id { get; set; } // This will match the PrefabEnum's underlying int value
/// <summary>
/// The name of the enum member (e.g., "Pending").
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The short name of the enum member (e.g., "Pend").
/// </summary>
public string? ShortName { get; set; }
/// <summary>
/// An optional description of the enum member.
/// </summary>
public string? Description { get; set; }
/// <inheritdoc />
public int SortOrder { get; set; }
/// <summary>
/// Default constructor.
/// </summary>
protected LookupEntity() { }
/// <summary>
/// Constructor to create from a PrefabEnum instance.
/// </summary>
protected LookupEntity(TEnum prefabEnum)
{
Id = prefabEnum.Value;
Name = prefabEnum.Name;
ShortName = null; // Set as needed
Description = null; // Set as needed
SortOrder = prefabEnum.Value; // Or set as needed
}
/// <summary>
/// Returns all PrefabEnum instances for this type.
/// </summary>
public static IReadOnlyList<TEnum> GetAllEnumInstances() => PrefabEnum<TEnum>.List;
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="Scrutor" Version="6.1.0" />
</ItemGroup>
</Project>

57
Prefab.Base/PrefabEnum.cs Normal file
View File

@@ -0,0 +1,57 @@
namespace Prefab.Base;
/// <summary>
/// Base type for rich enumerations that behave like enums but support additional metadata and behavior.
/// </summary>
/// <typeparam name="TEnum">Concrete enumeration type deriving from <see cref="PrefabEnum{TEnum}"/>.</typeparam>
public abstract class PrefabEnum<TEnum> where TEnum : PrefabEnum<TEnum>
{
private static readonly List<TEnum> Instances = [];
/// <summary>
/// Gets the canonical name of the enumeration value.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the integral value associated with the enumeration value.
/// </summary>
public int Value { get; }
/// <summary>
/// Initializes a new enumeration value and registers it in the global instance list.
/// </summary>
/// <param name="name">Logical name exposed to consumers.</param>
/// <param name="value">Numeric identifier for persistence.</param>
protected PrefabEnum(string name, int value)
{
Name = name;
Value = value;
Instances.Add(this as TEnum ?? throw new InvalidOperationException("PrefabEnum must be instantiated with a derived type."));
}
/// <summary>
/// Gets all declared enumeration values for <typeparamref name="TEnum"/>.
/// </summary>
public static IReadOnlyList<TEnum> List => Instances;
/// <summary>
/// Resolves an enumeration value by its logical name.
/// </summary>
/// <param name="name">The name to match.</param>
/// <returns>The matching enumeration.</returns>
/// <exception cref="ArgumentException">Thrown when no enumeration value matches the supplied name.</exception>
public static TEnum FromName(string name) =>
Instances.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
?? throw new ArgumentException($"Invalid name: {name}", nameof(name));
/// <summary>
/// Resolves an enumeration value by its integral value.
/// </summary>
/// <param name="value">The integer value to match.</param>
/// <returns>The matching enumeration.</returns>
/// <exception cref="ArgumentException">Thrown when no enumeration value matches the supplied value.</exception>
public static TEnum FromValue(int value) =>
Instances.FirstOrDefault(x => x.Value == value)
?? throw new ArgumentException($"Invalid value: {value}", nameof(value));
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Prefab.Base;
/// <summary>
/// Serializes <see cref="PrefabEnum{TEnum}"/> values as their logical names and deserializes by name or value.
/// </summary>
/// <typeparam name="TEnum">Enumeration type to convert.</typeparam>
public class PrefabEnumJsonConverter<TEnum> : JsonConverter<TEnum> where TEnum : PrefabEnum<TEnum>
{
/// <inheritdoc />
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch
{
JsonTokenType.String => PrefabEnum<TEnum>.FromName(reader.GetString()!),
JsonTokenType.Number => PrefabEnum<TEnum>.FromValue(reader.GetInt32()),
_ => throw new JsonException($"Unsupported token type '{reader.TokenType}' for Prefab enum deserialization.")
};
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) => writer.WriteStringValue(value.Name);
}

View File

@@ -0,0 +1,65 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Prefab.Base;
/// <summary>
/// Creates JSON converters that serialize Prefab enums using their logical names.
/// </summary>
public sealed class PrefabEnumJsonConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsClass)
{
return false;
}
var baseType = typeToConvert;
while (baseType is not null)
{
if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(PrefabEnum<>))
{
return true;
}
baseType = baseType.BaseType;
}
return false;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var converterType = typeof(PrefabEnumJsonConverter<>).MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(converterType)!;
}
/// <summary>
/// Converter used for concrete Prefab enum types.
/// </summary>
private sealed class PrefabEnumJsonConverter<TEnum> : JsonConverter<TEnum> where TEnum : PrefabEnum<TEnum>
{
/// <inheritdoc />
public override TEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var name = reader.GetString() ?? string.Empty;
return PrefabEnum<TEnum>.FromName(name);
}
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var value))
{
return PrefabEnum<TEnum>.FromValue(value);
}
throw new JsonException($"Unexpected token {reader.TokenType} when parsing {typeof(TEnum).Name}.");
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) => writer.WriteStringValue(value.Name);
}
}