Files
Migu2.0/MiGu.Server/Signal/SignalModelSchemaResolver.cs
T

276 lines
9.2 KiB
C#

using System.ComponentModel;
using System.Reflection;
using System.Text.Json.Serialization;
using StandardScene.Signal.Model;
namespace MiGu.Server.Signal;
/// <summary>
/// 从 StandardScene.Signal 程序集反射数据中心表和列。
/// 优先使用 MiGu.Server 编译期引用的程序集;否则再从 SimpleLite plugins 加载。
/// </summary>
public static class SignalModelSchemaResolver
{
private const string ModelNamespace = "StandardScene.Signal.Model";
private const string AssemblyFileName = "StandardScene.Signal.dll";
public static string? FindAssemblyPath(string? pluginsDir)
{
if (string.IsNullOrWhiteSpace(pluginsDir))
return null;
var path = Path.Combine(pluginsDir, AssemblyFileName);
return File.Exists(path) ? path : null;
}
public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
{
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
if (assembly == null)
return Array.Empty<SignalTableDef>();
try
{
return BuildTablesFromAssembly(assembly);
}
catch
{
return Array.Empty<SignalTableDef>();
}
}
public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
{
if (string.IsNullOrWhiteSpace(modelName))
return Array.Empty<SignalColumn>();
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
if (assembly == null)
return Array.Empty<SignalColumn>();
try
{
var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
if (type == null)
return Array.Empty<SignalColumn>();
return DiscoverColumns(type);
}
catch
{
return Array.Empty<SignalColumn>();
}
}
private static Assembly? TryGetSignalAssembly(string? pluginsDir, string? workingDirectory)
{
try
{
var referenced = typeof(PlcStationModel).Assembly;
if (HasModelTypes(referenced))
return referenced;
}
catch
{
/* 未引用插件工程时继续走 LoadFrom */
}
var assemblyPath = FindAssemblyPath(pluginsDir);
if (assemblyPath == null)
return null;
var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
ResolveEventHandler? handler = null;
handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
AppDomain.CurrentDomain.AssemblyResolve += handler;
try
{
var loaded = Assembly.LoadFrom(assemblyPath);
return HasModelTypes(loaded) ? loaded : null;
}
catch
{
return null;
}
finally
{
if (handler != null)
AppDomain.CurrentDomain.AssemblyResolve -= handler;
}
}
private static bool HasModelTypes(Assembly assembly)
{
return SafeGetTypes(assembly).Any(t =>
t != null && string.Equals(t.Namespace, ModelNamespace, StringComparison.Ordinal));
}
private static IReadOnlyList<string> BuildProbeDirs(string? pluginsDir, string? workingDirectory)
{
var dirs = new List<string>();
void Add(string? dir)
{
if (string.IsNullOrWhiteSpace(dir)) return;
var full = Path.GetFullPath(dir);
if (Directory.Exists(full) && !dirs.Contains(full, StringComparer.OrdinalIgnoreCase))
dirs.Add(full);
}
Add(pluginsDir);
Add(workingDirectory);
if (!string.IsNullOrWhiteSpace(pluginsDir))
Add(Path.GetDirectoryName(pluginsDir));
Add(AppContext.BaseDirectory);
return dirs;
}
private static Assembly? ResolveAssembly(string? assemblyName, IReadOnlyList<string> probeDirs)
{
if (string.IsNullOrWhiteSpace(assemblyName))
return null;
string simpleName;
try
{
simpleName = new AssemblyName(assemblyName).Name ?? assemblyName;
}
catch
{
simpleName = assemblyName.Split(',')[0];
}
foreach (var dir in probeDirs)
{
var path = Path.Combine(dir, simpleName + ".dll");
if (!File.Exists(path))
continue;
try
{
return Assembly.LoadFrom(path);
}
catch
{
/* try next dir */
}
}
return null;
}
private static IReadOnlyList<SignalTableDef> BuildTablesFromAssembly(Assembly assembly)
{
var found = new List<(int Order, SignalTableDef Table)>();
foreach (var type in SafeGetTypes(assembly))
{
if (type == null || !string.Equals(type.Namespace, ModelNamespace, StringComparison.Ordinal))
continue;
var attr = type.GetCustomAttributes(inherit: false)
.FirstOrDefault(a => a.GetType().Name == "SignalTableAttribute");
if (attr == null)
continue;
var attrType = attr.GetType();
var id = (attrType.GetProperty("Id")?.GetValue(attr) as string ?? "").Trim();
var fileName = (attrType.GetProperty("FileName")?.GetValue(attr) as string ?? "").Trim();
var title = (attrType.GetProperty("Title")?.GetValue(attr) as string ?? "").Trim();
var order = attrType.GetProperty("Order")?.GetValue(attr) as int? ?? 0;
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(fileName))
continue;
var category = type.GetCustomAttribute<CategoryAttribute>()?.Category ?? "";
if (string.IsNullOrWhiteSpace(title))
title = type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? id;
found.Add((order, new SignalTableDef(id, title, category, fileName, DiscoverColumns(type))));
}
return found.OrderBy(x => x.Order).ThenBy(x => x.Table.Id).Select(x => x.Table).ToList();
}
private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null)!;
}
}
private static IReadOnlyList<SignalColumn> DiscoverColumns(Type type)
{
var list = new List<SignalColumn>();
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
.Where(p => !IsJsonIgnored(p))
.OrderBy(p => p.MetadataToken))
{
var label = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName;
if (string.IsNullOrWhiteSpace(label))
label = prop.Name;
var columnType = MapType(prop.PropertyType);
string[]? options = null;
var group = prop.GetCustomAttribute<CategoryAttribute>(inherit: false)?.Category;
if (string.IsNullOrWhiteSpace(group))
group = null;
var select = ReadSelectOptions(prop);
if (select is { Length: > 0 })
{
columnType = "enum";
options = select;
}
else if (columnType == "enum" && (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType).IsEnum)
{
options = Enum.GetNames(Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
}
list.Add(new SignalColumn(prop.Name, label, columnType, options, group));
}
return list;
}
private static bool IsJsonIgnored(PropertyInfo prop)
{
if (prop.GetCustomAttribute<JsonIgnoreAttribute>() != null)
return true;
foreach (var attr in prop.GetCustomAttributes(inherit: true))
{
if (attr.GetType().FullName == "Newtonsoft.Json.JsonIgnoreAttribute")
return true;
}
return false;
}
private static string[]? ReadSelectOptions(PropertyInfo prop)
{
var attr = prop.GetCustomAttributes(inherit: false)
.FirstOrDefault(a => a.GetType().Name == "SignalSelectAttribute");
if (attr == null)
return null;
var options = attr.GetType().GetProperty("Options")?.GetValue(attr) as string[];
return options is { Length: > 0 } ? options : null;
}
private static string MapType(Type type)
{
var underlying = Nullable.GetUnderlyingType(type) ?? type;
if (underlying == typeof(bool))
return "bool";
if (underlying == typeof(int) || underlying == typeof(long) || underlying == typeof(short) ||
underlying == typeof(byte) || underlying == typeof(uint) || underlying == typeof(ulong))
return "int";
if (underlying.IsEnum)
return "enum";
return "string";
}
}