diff --git a/src/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs b/src/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs index 26454a53e..ad40d9e68 100644 --- a/src/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs +++ b/src/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs @@ -16,6 +16,13 @@ public class CS public string Version { get; set; } public string DbContext { get; set; } + /// + /// Whether this connection is active. Defaults to true for backward compatibility. + /// Set to false in appsettings.json to disable a connection without removing it — + /// useful for graceful degradation when a secondary database (e.g. Oracle) is unreachable. + /// + public bool Enabled { get; set; } = true; + public ConstructorInfo DcConstructor; private static List _cis; public static List Cis diff --git a/src/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs b/src/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs index f8ddaabf3..5aa5e6e43 100644 --- a/src/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs +++ b/src/WalkingTec.Mvvm.Core/Extensions/SystemExtensions/TypeExtension.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -204,6 +204,7 @@ public static Dictionary GetRandomValues(this Type self) string val = ""; var notmapped = pro.GetCustomAttribute(); if (notmapped == null && + pro.SetMethod != null && pro.PropertyType.IsList() == false && pro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) == false && skipFields.Contains(key) == false diff --git a/src/WalkingTec.Mvvm.Core/Support/DbConnectionWarmupService.cs b/src/WalkingTec.Mvvm.Core/Support/DbConnectionWarmupService.cs new file mode 100644 index 000000000..9ac4200e6 --- /dev/null +++ b/src/WalkingTec.Mvvm.Core/Support/DbConnectionWarmupService.cs @@ -0,0 +1,74 @@ +#nullable disable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WalkingTec.Mvvm.Core +{ + /// + /// Background service that pre-warms database connection pools on app startup. + /// Moves the cold-start latency (especially Oracle's ~60s first-query delay) + /// from the first user request to the app boot phase — runs in background, + /// does not block startup. + /// + /// For Oracle specifically, eliminates delay caused by: + /// - TNS name resolution and caching + /// - Connection pool creation (ODP.NET default Min Pool Size=0) + /// - ODP.NET internal metadata loading and self-tuning calibration + /// + /// The service is fault-tolerant: a connection failure is logged as a warning, + /// never crashes the app. + /// + public class DbConnectionWarmupService : BackgroundService + { + private readonly Configs _configs; + private readonly ILogger _logger; + + public DbConnectionWarmupService( + Microsoft.Extensions.Options.IOptionsMonitor configs, + ILogger logger) + { + _configs = configs.CurrentValue; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Small delay to let the rest of the app finish starting + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); + + var enabledConnections = _configs.Connections?.Where(x => x.Enabled).ToList(); + if (enabledConnections == null || enabledConnections.Count == 0) + { + return; + } + + _logger.LogInformation("[WTM] Starting database connection warm-up for {Count} connection(s)...", enabledConnections.Count); + + foreach (var cs in enabledConnections) + { + if (stoppingToken.IsCancellationRequested) break; + try + { + using var dc = cs.CreateDC(); + var dbContext = dc as Microsoft.EntityFrameworkCore.DbContext; + if (dbContext != null) + { + await dbContext.Database.CanConnectAsync(stoppingToken); + _logger.LogInformation("[WTM] Warm-up OK: connection '{Key}' ({DbType})", cs.Key, cs.DbType); + } + } + catch (Exception ex) + { + _logger.LogWarning("[WTM] Warm-up failed for connection '{Key}' ({DbType}): {Message}. First user query may experience delay.", + cs.Key, cs.DbType, ex.Message); + } + } + + _logger.LogInformation("[WTM] Database connection warm-up completed."); + } + } +} diff --git a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs index efeb7b8f1..de248672e 100644 --- a/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs +++ b/src/WalkingTec.Mvvm.Core/Support/FileHandlers/WtmFileProvider.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using WalkingTec.Mvvm.Core.Extensions; using WalkingTec.Mvvm.Core.Models; @@ -142,7 +143,7 @@ public IWtmFile GetFile(string id, bool withData = true, IDataContext dc = null) { dc = _wtm.CreateDC(); } - rv = dc.Set().CheckID(id).Select(x => new FileAttachment + rv = dc.Set().IgnoreQueryFilters().CheckID(id).Select(x => new FileAttachment { ID = x.ID, ExtraInfo = x.ExtraInfo, @@ -175,7 +176,7 @@ public void DeleteFile(string id, IDataContext dc = null) { dc = _wtm.CreateDC(); } - file = dc.Set().CheckID(id) + file = dc.Set().IgnoreQueryFilters().CheckID(id) .Select(x => new FileAttachment { ID = x.ID, @@ -210,7 +211,7 @@ public string GetFileName(string id, IDataContext dc = null) { dc = _wtm.CreateDC(); } - rv = dc.Set().CheckID(id).Select(x => x.FileName).FirstOrDefault(); + rv = dc.Set().IgnoreQueryFilters().CheckID(id).Select(x => x.FileName).FirstOrDefault(); if(rv == null) { rv = "unknown"; diff --git a/src/WalkingTec.Mvvm.Core/Utils.cs b/src/WalkingTec.Mvvm.Core/Utils.cs index 85ccfc639..cb0cb62f0 100644 --- a/src/WalkingTec.Mvvm.Core/Utils.cs +++ b/src/WalkingTec.Mvvm.Core/Utils.cs @@ -612,7 +612,7 @@ public static string GetCS(string cs, string mode, Configs config) return null; } - if (config.Connections.Any(x => x.Key.ToLower() == cs.ToLower()) == false) + if (config.Connections.Any(x => x.Key.ToLower() == cs.ToLower() && x.Enabled) == false) { cs = "default"; } @@ -623,7 +623,7 @@ public static string GetCS(string cs, string mode, Configs config) } if (mode?.ToLower() == "read") { - var reads = config.Connections.Where(x => x.Key.StartsWith(cs + "_")).Select(x => x.Key).ToList(); + var reads = config.Connections.Where(x => x.Key.StartsWith(cs + "_") && x.Enabled).Select(x => x.Key).ToList(); if (reads.Count > 0) { Random r = new Random(); diff --git a/src/WalkingTec.Mvvm.Core/WTMContext.cs b/src/WalkingTec.Mvvm.Core/WTMContext.cs index 642ed4600..b437f7cb8 100644 --- a/src/WalkingTec.Mvvm.Core/WTMContext.cs +++ b/src/WalkingTec.Mvvm.Core/WTMContext.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable disable using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; @@ -742,7 +742,12 @@ public virtual IDataContext CreateDC(bool isLog = false, string cskey = null, bo { cs = "default"; } - var rv = ConfigInfo.Connections.Where(x => x.Key.ToLower() == cs.ToLower()).FirstOrDefault().CreateDC(); + var csConfig = ConfigInfo.Connections.Where(x => x.Key.ToLower() == cs.ToLower()).FirstOrDefault(); + if (csConfig != null && !csConfig.Enabled) + { + throw new InvalidOperationException($"Database connection '{csConfig.Key}' ({csConfig.DbType}) is disabled. Enable it in appsettings.json (set Enabled: true)."); + } + var rv = csConfig.CreateDC(); rv.IsDebug = ConfigInfo.IsQuickDebug; rv.SetTenantCode(tenantCode); if (logerror == true) diff --git a/src/WalkingTec.Mvvm.Mvc/CodeGenVM.cs b/src/WalkingTec.Mvvm.Mvc/CodeGenVM.cs index e98d1d75c..780d8c6b8 100644 --- a/src/WalkingTec.Mvvm.Mvc/CodeGenVM.cs +++ b/src/WalkingTec.Mvvm.Mvc/CodeGenVM.cs @@ -76,10 +76,21 @@ public string MainDir int? index = EntryDir?.IndexOf($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Debug{Path.DirectorySeparatorChar}"); if (index == null || index < 0) { - index = EntryDir?.IndexOf($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Release{Path.DirectorySeparatorChar}") ?? 0; + index = EntryDir?.IndexOf($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}Release{Path.DirectorySeparatorChar}"); } - _mainDir = EntryDir?.Substring(0, index.Value); + if (index == null || index < 0) + { + // BaseDirectory does not contain \bin\Debug\ or \bin\Release\ + // (e.g. dotnet run from repo root, or published app). + // Fall back to the current working directory, which is typically + // the project source root in development. + _mainDir = Directory.GetCurrentDirectory(); + } + else + { + _mainDir = EntryDir!.Substring(0, index.Value); + } } return _mainDir; } @@ -1212,6 +1223,7 @@ public string GenerateTest() if (pro.Value == "$fk$") { var fktype = modelType.GetSingleProperty(pro.Key[0..^2])?.PropertyType; + if (fktype == null) continue; cpros += $@" v.{pro.Key} = Add{fktype.Name}();"; pros += $@" @@ -1301,6 +1313,7 @@ public string GenerateTest() private string GenerateAddFKModel(string keyname, Type t, List exist) { + if (t == null) return ""; if (exist == null) { exist = new List(); @@ -1319,7 +1332,7 @@ private string GenerateAddFKModel(string keyname, Type t, List exist) if (pro.Value == "$fk$") { var fktype = t.GetSingleProperty(pro.Key[0..^2])?.PropertyType; - if (fktype != t) + if (fktype != null && fktype != t) { rv += GenerateAddFKModel(pro.Key[0..^2], fktype, exist); } @@ -1332,7 +1345,7 @@ private string GenerateAddFKModel(string keyname, Type t, List exist) if (pro.Value == "$fk$") { var fktype = t.GetSingleProperty(pro.Key[0..^2])?.PropertyType; - if (fktype != t) + if (fktype != null && fktype != t) { cpros += $@" v.{pro.Key} = Add{fktype.Name}();"; diff --git a/src/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs b/src/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs index 12b2163fb..4bd68d3f7 100644 --- a/src/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs +++ b/src/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs @@ -542,11 +542,19 @@ public static IServiceCollection AddWtmContext(this IServiceCollection services, y.MultipartBodyLengthLimit = conf.FileUploadOptions.UploadLimit; }); services.AddHostedService(); - var cs = conf.Connections; + services.AddHostedService(); + var cs = conf.Connections.Where(x => x.Enabled).ToList(); foreach (var item in cs) { - var dc = item.CreateDC(); - dc.EnsureCreate(); + try + { + var dc = item.CreateDC(); + dc.EnsureCreate(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"[WTM] Warning: could not initialize database connection '{item.Key}' ({item.DbType}): {ex.Message}"); + } } services.AddVersionedApiExplorer(o=> { diff --git a/src/WalkingTec.Mvvm.Mvc/Helper/WtmElsaContext.cs b/src/WalkingTec.Mvvm.Mvc/Helper/WtmElsaContext.cs index 09c2ebe25..d3c0e77c1 100644 --- a/src/WalkingTec.Mvvm.Mvc/Helper/WtmElsaContext.cs +++ b/src/WalkingTec.Mvvm.Mvc/Helper/WtmElsaContext.cs @@ -13,7 +13,10 @@ public override string Schema { get { - if (Database.IsOracle()) + // MySQL and Oracle do not support schemas the way SQL Server does. + // MySQL treats schema = database, so returning a schema prefix like "Elsa" + // would cause EF Core to look for a different database and fail to create tables. + if (Database.IsOracle() || Database.ProviderName?.Contains("MySql") == true) { return null; } diff --git a/src/WalkingTec.Mvvm.Mvc/_CodeGenController.cs b/src/WalkingTec.Mvvm.Mvc/_CodeGenController.cs index e492ff470..4fc019b2c 100644 --- a/src/WalkingTec.Mvvm.Mvc/_CodeGenController.cs +++ b/src/WalkingTec.Mvvm.Mvc/_CodeGenController.cs @@ -131,7 +131,9 @@ private List GetAllModels() var models = new List(); //获取所有模型 - var pros = Wtm.ConfigInfo.Connections.SelectMany(x => x.DcConstructor.DeclaringType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)); + var pros = Wtm.ConfigInfo.Connections + .Where(x => x.Enabled && x.DcConstructor != null) + .SelectMany(x => x.DcConstructor.DeclaringType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)); if (pros != null) { foreach (var pro in pros) diff --git a/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs b/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs index 86f7d00b3..b3d9e8ee0 100644 --- a/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs +++ b/src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs @@ -352,10 +352,14 @@ public IActionResult UploadImage([FromServices] WtmFileProvider fp, string sm = } var FileData = Request.Form.Files[0]; - Image oimage = Image.Load(FileData.OpenReadStream()); - if (oimage == null) + Image oimage; + try + { + oimage = Image.Load(FileData.OpenReadStream()); + } + catch (Exception) { - return JsonMore(new { Id = string.Empty, Name = string.Empty }, StatusCodes.Status404NotFound); + return JsonMore(new { Id = string.Empty, Name = string.Empty }, StatusCodes.Status400BadRequest); } if (width == null) { diff --git a/src/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs b/src/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs index 9990068d2..93abcb3d5 100644 --- a/src/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs +++ b/src/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs @@ -214,15 +214,11 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { listItems = (Items.Model as IEnumerable).ToList(); } - foreach (var item in listItems) + if (selectVal.Count > 0) { - if (selectVal.Contains(item.Value?.ToString())) + foreach (var item in listItems) { - item.Selected = true; - } - else - { - item.Selected = false; + item.Selected = selectVal.Contains(item.Value?.ToString()); } } }