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());
}
}
}