using System.ComponentModel; using System.Reflection; using System.Text.Json.Serialization; using StandardScene.Signal.Model; namespace MiGu.Server.Signal; /// /// 从 StandardScene.Signal 程序集反射数据中心表和列。 /// 优先使用 MiGu.Server 编译期引用的程序集;否则再从 SimpleLite plugins 加载。 /// 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 ResolveTables(string? pluginsDir, string? workingDirectory = null) { var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory); if (assembly == null) return Array.Empty(); try { return BuildTablesFromAssembly(assembly); } catch { return Array.Empty(); } } public static IReadOnlyList ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null) { if (string.IsNullOrWhiteSpace(modelName)) return Array.Empty(); var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory); if (assembly == null) return Array.Empty(); try { var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true); if (type == null) return Array.Empty(); return DiscoverColumns(type); } catch { return Array.Empty(); } } 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 BuildProbeDirs(string? pluginsDir, string? workingDirectory) { var dirs = new List(); 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 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 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()?.Category ?? ""; if (string.IsNullOrWhiteSpace(title)) title = type.GetCustomAttribute()?.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 SafeGetTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t != null)!; } } private static IReadOnlyList DiscoverColumns(Type type) { var list = new List(); foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0) .Where(p => p.GetCustomAttribute()?.Browsable != false) .Where(p => !IsJsonIgnored(p)) .OrderBy(p => p.MetadataToken)) { var label = prop.GetCustomAttribute()?.DisplayName; if (string.IsNullOrWhiteSpace(label)) label = prop.Name; var columnType = MapType(prop.PropertyType); string[]? options = null; var group = prop.GetCustomAttribute(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() != 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"; } }