分类 Net core 下的文章

开发技术上,微软走下坡路的一大原因,除了开源生态不够,还有一个很重要的原因,很多常用的功能,不能简单的代码实现,这是很多微软系程序猿头疼一件事。

比如,要想做个简单的 http server,需要很多代码,最后,性能跟IIS还相差十万八千里
比如,要搭个FTP,非要搞得很复杂,跟系统用户挂钩,不能简单的用户名、密码、目录
比如,要做个最简单的md5加密,还得好几行代码,还不是常见的写法,每次得去搜索
比如,得到一个时间戳,也是一样得好几行代码,敲很多文字
比如,读取数据库到对象,还要自己去实现 DataTableToEntityList
…………

太多了,最近在使用 net core 开发 控制台应用程序时,为了读取一个 json 配置文件,被折磨得不行了。。。。
网上的资源都是比较老的,读取时,无法解决错误:

IConfiguration builder = new ConfigurationBuilder()

这个简单的语句,怎么都是提示
错误 CS0246 未能找到类型或命名空间名“ConfigurationBuilder”(是否缺少 using 指令或程序集引用?)

我加了这些 Nuget 引用:

Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.Configuration.EnvironmentVariables
Microsoft.Extensions.Configuration.FileExtensions

仍然没用,还是找不到!
真无语,这种常用的功能,居然要用微软自己的东西来实现这么麻烦,于是手动写了一个,而且增加了监控功能,当文件有修改时,自动读取新的配置。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CommProcess
{
    /// <summary>
    /// Json 配置读取类
    /// </summary>
    public class JsonConfig
    {
        private JObject JsonObj;
        private readonly string JsonFilePath;
        private readonly FileSystemWatcher JsonWatcher;

        /// <summary>
        /// Json 配置读取类
        /// </summary>
        /// <param name="JsonFilePath">Json 文件路径,绝对或相对路径</param>
        /// <param name="MonitorChange">是否监视配置文件改动</param>
        public JsonConfig(string JsonFilePath, bool MonitorChange = true)
        {
            this.JsonFilePath = JsonFilePath;
            ReadFile();

            if (MonitorChange)
            {
                if (JsonFilePath.IndexOf(':') != 1)
                {
                    JsonFilePath = Path.Combine(Environment.CurrentDirectory, JsonFilePath);
                }
                JsonWatcher = new FileSystemWatcher();
                JsonWatcher.Path = Path.GetDirectoryName(JsonFilePath);
                JsonWatcher.Filter = Path.GetFileName(JsonFilePath);
                JsonWatcher.EnableRaisingEvents = true;
                JsonWatcher.Changed += JsonWatcher_Changed;
            }
        }

        private void JsonWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            Task.Run(() =>
            {
                Task.Delay(1000);
                ReadFile();
            });
        }

        private void ReadFile()
        {
            using var stream = File.Open(JsonFilePath, FileMode.Open, FileAccess.Read);
            byte[] b = new byte[stream.Length];
            stream.Read(b, 0, b.Length);
            string json = Encoding.UTF8.GetString(b);
            JsonObj = JObject.Parse(json);
        }

        /// <summary>
        /// 获取配置值
        /// </summary>
        /// <typeparam name="T">数据类型</typeparam>
        /// <param name="node">路径,比如:config.server</param>
        /// <returns></returns>
        public T? Get<T>(string node)
        {
            var jnode = JsonObj.SelectToken(node);
            if (jnode == null) return default;
            return jnode.Value<T>();
        }
    }
}

使用很简单,

var jconfig = new JsonConfig("jsconfig.json");
var name = jconfig.Get<string>("info.name");

Session 是比较老的服务端单会话缓存实现,在新的 net core 中如果需要使用 session,需要增加服务注入,其实直接使用内置的 TempData 替代使用更方便。

TempData 是在会话有效的过程中长期有效的,只是在正常获取一次值 TempData[key] 后,就会被删除。但可以简单的通过:

value = TempData.Peek(key)

的方式来获取值,就不会被删除,等同于 Session 的效果了。

对于在服务器上运行的内存需求较大的程序,建议开启 Server GC 模式,有利于提升程序的性能。

挺简单,不管是 windows 还是 Linux ,只需要增加环境变量即可:

export DOTNET_gcServer=1
export DOTNET_GCHeapCount=c

也可以在配置文中进行配置
.NET Core 在文件 runtimeconfig.json

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    ...
    <runtime>
        <gcServer enabled="true"/>
        <GCHeapCount>0xc</GCHeapCount>
    </runtime>
</configuration>

.NET Framework 在 app.config 文件

{
  "runtimeOptions": {
   "configProperties": {
      "System.GC.Server": true,
      "System.GC.HeapCount": 12
   }
  }
}

本来最简单的做法,就是将语言选择放在cookie中,但出于对SEO的考虑,这样做无法让搜索引擎抓取多语种页面,最好的做法,还是不同语言有不同的路径,例如:

http://www.xxx.com/zh-ch/products
http://www.xxx.com/en-us/products
....

静态类 Langs.cs 中,定义可以用的语言:

    public readonly static string[] LangCodes = new string[] { "vi-vn", "en-us", "zh-cn" };

识别用的路由约束类,其中的 "auto" 是特殊定义的,主要用于用户第一次进入时对用户的 Accept-Languages 进行自动识别:

public class LanguageRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.ContainsKey("culture"))
            return false;

        var culture = values["culture"]?.ToString();
        if (culture == null) return false;
        return culture == "auto" || Langs.LangCodes.Contains(culture);
    }
}

路由约束:

public class LanguageRouteConstraint : IRouteConstraint
{
    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.ContainsKey("culture"))
            return false;

        var culture = values["culture"]?.ToString();
        if (culture == null) return false;
        return culture == "auto" || Langs.LangCodes.Contains(culture);
    }
}

然后在 program.cs 中配置:

builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.Add(new CulturePageRouteConvention());
}).AddRazorRuntimeCompilation();

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("culture", typeof(LanguageRouteConstraint));
});

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("culture", typeof(LanguageRouteConstraint));
});

路由:

app.MapControllerRoute(name: "default", pattern: "{culture:culture=auto}/{controller:slugify=Home}/{action:slugify=Index}/{id?}");

很多人会直接用静态类做内存缓存,包括我自己之前也是这样做,但这样内存的生命周期及GC机制得不到保障,特别是对于多线程的控制,静态类都是无能为力。

另外就是 MemoryCache,很多人都习惯用他的 MemoryCache.TryGetValue 来实现自动缓存,这对于小项目及访问量不大的项目是可行的,但如果对于大项目,这样会导致访问很慢,因为是被动式的去拉取数据,有没有一种更好的方式呢,好在net core 提供了 BackgroundService 及 IHostedService,两者类似,可以完美的解决我们的需求,这里我就举例说一下使用 IHostedService 来实现主动式缓存网站首页的全部数据,以实现最快速的首页访问。

新建逻辑处理服务类:

public class DbServices : IHostedService, IDisposable
{
    CancellationToken cancelToken;
    HomeData? datas;
    Task? mainTask;
    private ILogger logger;

    public DbServices(ILogger logger)
    {
        this.logger=logger;
    }

    public HomeData? GetHomeData()
    {
        return datas;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        cancelToken = cancellationToken;
        mainTask = Task.Factory.StartNew(HomeDataWork, cancellationToken);
        return Task.CompletedTask;
    }
    private async void HomeDataWork()
    {
        DateTime last = DateTime.Now.AddMinutes(-2);
        while (!cancelToken.IsCancellationRequested)
        {
            int sec = 30000 - (int)(DateTime.Now - last).TotalMilliseconds;
            if (sec > 0)
                await Task.Delay(sec);
            last = DateTime.Now;
            HomeData? _data = .....;//实现获取数据
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        mainTask?.Dispose();
    }
}

Program.cs 中:

builder.Services.AddSingleton<DbServices>();
builder.Services.AddHostedService<DbServices>(provider => provider.GetService<DbServices>());

大功造成,该服务会在后台主动更新内存数据,首页可以直接无任何延迟直接获取数据:

DbServices? service = MyHttpContext.Current?.RequestServices.GetService(typeof(DbServices)) as DbServices;
HomeData = service?.GetHomeData(Langs.Instance.LangCode) ?? new();