Skip to content
7 changes: 7 additions & 0 deletions src/WalkingTec.Mvvm.Core/ConfigOptions/CS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public class CS
public string Version { get; set; }
public string DbContext { get; set; }

/// <summary>
/// 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.
/// </summary>
public bool Enabled { get; set; } = true;

public ConstructorInfo DcConstructor;
private static List<ConstructorInfo> _cis;
public static List<ConstructorInfo> Cis
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
Expand Down Expand Up @@ -204,6 +204,7 @@ public static Dictionary<string, string> GetRandomValues(this Type self)
string val = "";
var notmapped = pro.GetCustomAttribute<NotMappedAttribute>();
if (notmapped == null &&
pro.SetMethod != null &&
pro.PropertyType.IsList() == false &&
pro.PropertyType.IsSubclassOf(typeof(TopBasePoco)) == false &&
skipFields.Contains(key) == false
Expand Down
74 changes: 74 additions & 0 deletions src/WalkingTec.Mvvm.Core/Support/DbConnectionWarmupService.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class DbConnectionWarmupService : BackgroundService
{
private readonly Configs _configs;
private readonly ILogger<DbConnectionWarmupService> _logger;

public DbConnectionWarmupService(
Microsoft.Extensions.Options.IOptionsMonitor<Configs> configs,
ILogger<DbConnectionWarmupService> 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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -142,7 +143,7 @@ public IWtmFile GetFile(string id, bool withData = true, IDataContext dc = null)
{
dc = _wtm.CreateDC();
}
rv = dc.Set<FileAttachment>().CheckID(id).Select(x => new FileAttachment
rv = dc.Set<FileAttachment>().IgnoreQueryFilters().CheckID(id).Select(x => new FileAttachment
{
ID = x.ID,
ExtraInfo = x.ExtraInfo,
Expand Down Expand Up @@ -175,7 +176,7 @@ public void DeleteFile(string id, IDataContext dc = null)
{
dc = _wtm.CreateDC();
}
file = dc.Set<FileAttachment>().CheckID(id)
file = dc.Set<FileAttachment>().IgnoreQueryFilters().CheckID(id)
.Select(x => new FileAttachment
{
ID = x.ID,
Expand Down Expand Up @@ -210,7 +211,7 @@ public string GetFileName(string id, IDataContext dc = null)
{
dc = _wtm.CreateDC();
}
rv = dc.Set<FileAttachment>().CheckID(id).Select(x => x.FileName).FirstOrDefault();
rv = dc.Set<FileAttachment>().IgnoreQueryFilters().CheckID(id).Select(x => x.FileName).FirstOrDefault();
if(rv == null)
{
rv = "unknown";
Expand Down
4 changes: 2 additions & 2 deletions src/WalkingTec.Mvvm.Core/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand All @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions src/WalkingTec.Mvvm.Core/WTMContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions src/WalkingTec.Mvvm.Mvc/CodeGenVM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 += $@"
Expand Down Expand Up @@ -1301,6 +1313,7 @@ public string GenerateTest()

private string GenerateAddFKModel(string keyname, Type t, List<Type> exist)
{
if (t == null) return "";
if (exist == null)
{
exist = new List<Type>();
Expand All @@ -1319,7 +1332,7 @@ private string GenerateAddFKModel(string keyname, Type t, List<Type> 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);
}
Expand All @@ -1332,7 +1345,7 @@ private string GenerateAddFKModel(string keyname, Type t, List<Type> 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}();";
Expand Down
14 changes: 11 additions & 3 deletions src/WalkingTec.Mvvm.Mvc/Helper/FrameworkServiceExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -542,11 +542,19 @@ public static IServiceCollection AddWtmContext(this IServiceCollection services,
y.MultipartBodyLengthLimit = conf.FileUploadOptions.UploadLimit;
});
services.AddHostedService<QuartzHostService>();
var cs = conf.Connections;
services.AddHostedService<DbConnectionWarmupService>();
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=>
{
Expand Down
5 changes: 4 additions & 1 deletion src/WalkingTec.Mvvm.Mvc/Helper/WtmElsaContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion src/WalkingTec.Mvvm.Mvc/_CodeGenController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ private List<Type> GetAllModels()
var models = new List<Type>();

//获取所有模型
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)
Expand Down
10 changes: 7 additions & 3 deletions src/WalkingTec.Mvvm.Mvc/_FrameworkController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
10 changes: 3 additions & 7 deletions src/WalkingTec.Mvvm.TagHelpers.LayUI/Form/ComboBoxTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,11 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
{
listItems = (Items.Model as IEnumerable<ComboSelectListItem>).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());
}
}
}
Expand Down
Loading