公司项目是FW4.5.1,近期准备迁移到Core,并放到Coding上。
设计项目较多,每次迁移都一直回想步骤,有点费脑。
所以还是写篇文章,记录下流程
一方面是给别人看,另一方面是以防自己忘记,后续的迁移,也能无脑对着文章操作就行。

一、环境、版本说明

FW Web -> Core Web 3.1
FW 库 -> Standard 2.0
数据存储:Redis、MSSQL
配置中心:Apollo
Ops:Coding
代码管理:Git
部署载体: Docker

二、代码准备工作

因为之前遇到过一次sln、suo、csproj变动造成了代码提交和jenkins编译不通过,所以以最笨的方式,先按照原项目创建新的项目内容。
当然,你觉得新建麻烦,而你对项目结构熟悉,也可以尝试直接改csproj。

迁移.net framework 工程到.net core

不过,我这边为了稳,就新建吧。

1.新建Core Web项目和文件转移

第一步就是新建Core Web项目,在添加推荐取消Https的支持,会有一些新手不会使用,导致无法打开网页(其实打开的时候添加信任证书就行),所以避免麻烦,我一般都是去掉。
而我这边用的Core是3.1版本,Standard是 2.0
而不同库的文件,只要不是特殊文件,*.cs一般都直接拷贝过去就行.
但需要注意,Core没有Properties.AssemblyInfo.cs,而且Web.config对web也有影响,不要多拷,造成项目文件混乱了.

因为刚入社会时,带我的大佬经常很谨慎,我被他的习惯感染,文件的迁移,一般都是一个个确认,Ctrl++++++去拷贝的,时间有余的话,我也推荐对工作有这种谨慎的思想.

文件转移完,我也会对一下,哪些时旧项目多余的文件,引用有没有少文件
尽量都是自己操作,对项目更了解.可我这边删的就多了
比如项目没有视图,本身只提供Api,那在新的项目中,没必要保留View文件夹,优化好项目目录
这对不是特别大的项目学习有一点的帮助
image.png

2.更新必要的引用

接下来,就是先让编译正常,这就要引用必要的nuget包,这里需要注意,Core可以引用FW的包运行,但不能真正去跑一些只有FW的内容
我这边有些包命名有一点的混淆,这样可以通过看下旧项目安装了哪些,跟着装就行。当然,如果你知道哪些是不必须要的,也可以为了项目干净,而不去引用
image.png

3.对于Core一些写法不同的地方

1.HttpContext

Core已经没有了全局的HttpContext,现在能从拦截器或启动时注入获取
一般FW升级到Core都会兼容旧的写法,下面是一种方案

HttpContext.cs可以考虑放到Common或者需要的那个库
    public static class HttpContext
    {
        private static IHttpContextAccessor _accessor;

        public static Microsoft.AspNetCore.Http.HttpContext Current => _accessor.HttpContext;

        public static void Configure(IHttpContextAccessor accessor)
        {
            _accessor = accessor;
        }
    }
StaticHttpContextExtensions.cs,放到Web项目中
    /// <summary>
    /// 兼容原来的HttpContext引用的静态类;注意在Service中使用,若有其他地方使用,后续可将HttpContext移至其他公共库中。
    /// </summary>
    public static class StaticHttpContextExtensions
    {
        public static void AddHttpContextAccessor(this IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        }

        public static IApplicationBuilder UseStaticHttpContext(this IApplicationBuilder app)
        {
            var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
            MessageServiceCenter.Service.HttpContext.Configure(httpContextAccessor);
            return app;
        }
    }
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticHttpContext();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
1.System.Web.Security哈希

Core没有了System.Web.Security,自己实现差不多得功能就行
Encoding.UTF8.GetBytes(System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(sKey, "SHA1").Substring(0, 8));
换成
Encoding.UTF8.GetBytes(SHA(sKey).Substring(0, 8));

EncryptHelper.cs
        /// <summary>
        /// MD5加密
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static string Md5(string s)
        {

            using (var md5 = MD5.Create())
            {
                var result = md5.ComputeHash(Encoding.UTF8.GetBytes(s));
                var strResult = BitConverter.ToString(result);
                return strResult.Replace("-", "").ToUpper();
            }

        }

        /// <summary>
        /// SHA
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static string SHA(string s)
        {
            using (var sha = SHA1.Create())
            {
                var result = sha.ComputeHash(Encoding.UTF8.GetBytes(s));
                var strResult = BitConverter.ToString(result);
                return strResult.Replace("-", "").ToUpper();
            }
        }
3.FilterAttribute

actionExecutedContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.NotImplemented);
可以参考换成

actionExecutedContext.Result = new ContentResult
{
    StatusCode = (int)HttpStatusCode.NotImplemented,
    ContentType = "application/json;charset=utf-8"
};

注意新的OnActionExecuting入参变化,从HttpActionContext变为ActionExecutingContext
原本的ActionExecutingContext.Request变成了ActionExecutingContext.HttpContext.Request

HeadersContains变为ContainsKey,GetValues变成直接Headers[""]取值

目前我这边遇到会无法通过编译的不同点就这么多,后续发现再补充
而Core想正常跑起来还是有很多其他的调整,可以继续往下看
image.png

3.调整Core的依赖注入序列化等基本配置

1.Json序列化编码和过滤器

我这边序列化不使用原生的System.Text.Json
原因可参考netcore 3.1 json序列号时间和emoji格式相关问题
这里加了DatetimeJsonConverter是因为Newtonsoft时间转换IsoDateTimeConverter设置了DefaultDateTimeFormat值得情况下,是直接调用了DateTime.ParseExact,如果前端同时会传2020-01-012020-01-01 12:00:00两种格式的话,前者会无法反序列化
所以要加一个解析类

Startup.cs
services.AddControllers(op =>
{
    //过滤器
    op.Filters.Add<Attribute>();
    op.Filters.Add<Attribute>();
}).AddNewtonsoftJson(options =>
{
    options.SerializerSettings.ContractResolver = new DefaultContractResolver();//移除默认驼峰格式
    options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
    options.SerializerSettings.Converters.Add(new DatetimeJsonConverter());
});
DatetimeJsonConverter.cs
    /// <summary>
    /// Newtonsoft.Json自定义时间格式转换
    /// </summary>
    public class DatetimeJsonConverter : DateTimeConverterBase
    {
        //读取对象,DateTimeFormat为空时就不按DateTimeFormat的格式强制序列化,"2021-08-29"这种只有日期的字符串也能序列化成功
        private static IsoDateTimeConverter isoDateTimeConverterRaad = new IsoDateTimeConverter() { };
        //写对象,输出时间格式为"yyyy-MM-dd HH:mm:ss"
        private static IsoDateTimeConverter isoDateTimeConverterWrite = new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" };

        /// <summary>
        /// 重写输入时的时间序列化格式
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="objectType"></param>
        /// <param name="existingValue"></param>
        /// <param name="serializer"></param>
        /// <returns></returns>
        public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            object result = null;
            try
            {
                result = isoDateTimeConverterRaad.ReadJson(reader, objectType, existingValue, serializer);
            }
            catch (Exception)
            {
            }
            return result;
        }
        /// <summary>
        /// 重写输出时的时间格式
        /// </summary>
        /// <param name="writer"></param>
        /// <param name="value"></param>
        /// <param name="serializer"></param>
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            isoDateTimeConverterWrite.WriteJson(writer, value, serializer);
        }

    }

IsoDateTimeConverter的实现

2.Controller

Core对控制器的使用跟FW有些不同,需要对控制器添加[ApiController]标签,不然打开后,Swagger会出现下面异常
Action require a unique method/path ...
所以推荐用一个BaseControllerClass去做兜底,而其他控制器全部继承它
一来统一了控制器的父类,好做后续子的调整,二来避免改漏

[ApiController]
[Route("api/[controller]/[action]")]
public class BaseController : ControllerBase{}
3.IOC

我这边旧项目是用到了Autofac,所以需要调整为Core的IOC,这个就不多解释了,是必须掌握的知识,不清楚就赶紧去学吧!
services.AddScoped(typeof(IRepository<>), typeof(Respository<>));

3.ModelState

对于使用了ModelState需要注意在Core中自带就有一套验证,但我们已经有自己的一套验证,所以需要禁用了MVC自带的验证返回

services.Configure<ApiBehaviorOptions>((o) =>
{
    o.SuppressModelStateInvalidFilter = true;
});
4.DataTime.ToString时间格式

对于Core的时间格式,是获取系统的设置进行格式化的
在一些环境下,时间格式跟win不一样,比如Liunx执行DataTime.Now.ToString()出来的就可能是5/20/2022 15:00
有些时候我们不能使用这种格式,那么就可以调整这个默认的格式
这里需要注意下,ToString的格式是ShortDatePattern+LongTimePattern不要调整错参数了,不然就没效果了
代码如下

Program.cs
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("zh-CN", true)
{
    DateTimeFormat = {
        ShortTimePattern = "H:mm:ss",
        ShortDatePattern = "yyyy/M/d",
        FullDateTimePattern = "yyyy/M/d HH:mm:ss",
        LongDatePattern = "yyyy/M/d",
        LongTimePattern = "HH:mm:ss"
    }
};
233.Core 3.1 多斜杠问题

直接就是说3.1不支持多余的斜杠了,忘记N5支不支持,后续我再翻下文档
因为我这边的项目很多其他项目调用,有不确定的多斜杠问题,所以目前解决方式是加管道,移除请求进来时多余的斜杠
需要注意管道添加的位置,最好在进入时处理

RewriteRouteRule.cs
/// <summary>
/// 重写url规则
/// </summary>
public class RewriteRouteRule
{
    /// <summary>
    /// 处理url中的多斜杠("//")问题
    /// </summary>
    /// <param name="context"></param>
    public static void ReWriteRequests(RewriteContext context)
    {
        var request = context.HttpContext.Request;
        if (request.Path.Value.Contains("//"))
        {
            string[] splitlist = request.Path.Value.Split("/", StringSplitOptions.RemoveEmptyEntries);
            var newpath = "/" + string.Join("/", splitlist);
            request.Path = newpath;
        }
    }
}
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRewriter(new RewriteOptions().Add(RewriteRouteRule.ReWriteRequests));
}

三、在Coding创建代码仓库和提交代码

进入Coding项目,点击左侧菜单代码仓库,再从右上角创建代码仓库
记下仓库的地址
image.png

好了,仓库准备完毕,接下来处理代码提交

1.初始化Git

进入项目根目录,执行Git仓库初始化脚本git init
PS C:\**\**> git init
Initialized empty Git repository in C:/**/**/.git/

2.设置源

这里源填刚才创建的仓库的连接
git remote add origin **/**.git

3.添加Git文件和忽略等配置文件(可选)

这里有些公司会有文件忽略要求,推荐在第一次提交前吧这些处理了,如.gitignoreDocker文件
当然如果有需要,你可以在VS上操作Git的忽略也是没问题的
确认没有少文件就能提交代码(可直接用VS操作)
git add .
git commit -am "首次提交项目代码" //提交变更
git push origin master //推送到源仓库

结果实例:

PS C:\**\**> git push origin master
Enumerating objects: 245, done.
Counting objects: 100% (245/245), done.
Delta compression using up to 2 threads
Compressing objects: 100% (239/239), done.
Writing objects: 100% (245/245), 768.18 KiB | 1.74 MiB/s, done.
Total 245 (delta 110), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (110/110), done.
To ***/***.git
 * [new branch]      master -> master

至此,你就能在Coding看到你刚才提交的代码了
image.png

四、部署

思路和流程:Coding不但提供了代码仓库,还提供了项目构建、脚本执行的功能
所以我们可以利用Coding的服务器帮助我们构建Core项目,然后将包打包成镜像,推到我们的服务器上

1.1确认项目编译成功

dotnet publish .\***.WebApi\***.WebApi.csproj -o publish

执行成功提示例子:

  MiniCenter.WebApi -> D:\**.WebApi\bin\Debug\netcoreapp3.1\**.WebApi.dll
  MiniCenter.WebApi -> D:\**\publish\

1.2使用WSL2确认镜像打包是否正常

docker build -t *:vtest -f Dockerfile .

Successfully built *
Successfully tagged *:vtest

1.3在Coding创建制品库

image.png
image.png
在出来的页面输入自己的Coding账号密码,跟Github那种感觉差不多
然后生成自己的令牌,这个需要记一下,后续我们要用
这里也推荐先复制到WSL2里面运行一下,确认返回了登录成功字样:

Login Succeeded

点击左侧推送,填入我们项目信息,他会帮我们生成命令,我们直接在WSL2执行就行
image.png
image.png
执行完后,可以执行docker images看到我们的镜像
当然在Coding上也能看到推送上去了
image.png

2.1添加项目构建计划

image.png
image.png
这里的计划随便填就行,反正我们后面直接修改脚本内容
重要的是计划名称和代码仓库不错就行。
编译构建使用dotnet3.1是因为这是平台构造环境设置的命令

2.2创建项目构建计划

创建成功后,会显示我们构建流程的图形化编辑器,我们直接在这里调整
image.png

2.3设置构造环境

第一步需要先确认我们每个步骤的构造环境,因为流程安排是编译完打包镜像远程推送的,所以全部选择默认构造环境就行
不然错误的环境会导致一些指令在在环境中不一定有,导致整体构造失败
image.png

2.4设置编辑流程脚本

image.png
在编辑,我们执行两个脚本,一是设置dotnet环境nuget,二是编译
因为我这边使用到了一些自己的包,所以用了自己的服务器,这里按照自己的需求就行。
nuget这里使用了变量,方便后面变更
编译基本跟我们自己在本地编译一样,只是因为coding构造环境命令问题,我这边需要换成dotnet3.1
如果你是拷贝在PS执行的命令,需要注意这里是在linux执行的,要替换掉win路径的反斜杠

if [ "$(dotnet3.1 nuget list source | grep ${nuget_service})" = "" ]; then
echo "准备设置nuget ${nuget_service}"
dotnet3.1 nuget add source "http://${nuget_service}/v3/index.json"
else
echo "nuget已设置 ${nuget_service}"
fi
dotnet3.1 publish ./**.WebApi/**.WebApi.csproj -o publish

2.5设置镜像构建和远程推送

这里也是使用我们自己的脚本构建
因为不知为何,平台jenkins的docker会随机报Cannot retrieve .Id from 'docker inspect base',虽说提了工单,但得看平台什么时候处理了

我们先填入 Shell 脚本,命令都是我们刚才执行过的制品库和docker打包镜像的命令,WSL2直接敲入history查看执行过的命令
也方便我们拷贝

#移动编译的文件
mv publish/ ./***/
#进入项目中
cd ./***/
#docker 编译打包镜像
docker build -t ***:vtest -f Dockerfile .
#docker 远程登录
docker login -u docker-*** -p *** ***.net
#docker 打远程标签
docker tag ***:vtest ***.net/***/docker/***:latest
#docker 推送到制品库
docker push ***.net/***/docker/***:latest

image.png

2.6设置部署到远程服务器脚本

这一步其实没多少参考,大部分需要按照你的情况去编写命令
我这里提供了一份脱敏的脚本,可以参考。
主要流程是登录远程服务器,然后卸载现有docker容器,重新run

参考脚本
def remoteConfig = [:]
remoteConfig.name = "my-remote-server"
remoteConfig.host = "${REMOTE_HOST}"
remoteConfig.port = "${REMOTE_SSH_PORT}".toInteger()
remoteConfig.allowAnyHosts = true

withCredentials([
  sshUserPrivateKey(
    credentialsId: "${REMOTE_CRED}",
    keyFileVariable: "privateKeyFilePath"
  ),
  usernamePassword(
    credentialsId: "${CODING_ARTIFACTS_CREDENTIALS_ID}",
    usernameVariable: 'CODING_DOCKER_REG_USERNAME',
    passwordVariable: 'CODING_DOCKER_REG_PASSWORD'
  )
]) {
  // SSH 登陆用户名
  remoteConfig.user = "${REMOTE_USER_NAME}"
  // SSH 私钥文件地址
  remoteConfig.identityFile = privateKeyFilePath

  echo "docker login -u ${CODING_DOCKER_REG_USERNAME} -p ${CODING_DOCKER_REG_PASSWORD} ${CODING_DOCKER_REG_HOST}"


  // 请确保远端环境中有 Docker 环境
  sshCommand(
    remote: remoteConfig,
    command: "docker login -u ${CODING_DOCKER_REG_USERNAME} -p ${CODING_DOCKER_REG_PASSWORD} ${CODING_DOCKER_REG_HOST}",
    sudo: false,
  )

  sshCommand(
    remote: remoteConfig,
    command: "docker rm -f ${DOCKER_IMAGE_NAME} | true",
    sudo: false,
  )

  // DOCKER_IMAGE_VERSION 中涉及到 GIT_LOCAL_BRANCH / GIT_TAG / GIT_COMMIT 的环境变量的使用
  // 需要在本地完成拼接后,再传入到远端服务器中使用
  DOCKER_IMAGE_URL = sh(
    script: "echo ***.net/middleground/docker/${DOCKER_IMAGE_NAME}:latest",
    returnStdout: true
  )

  remoteConfig.runCom="docker run -d -v /etc/localtime:/etc/localtime:ro  -p ${host_port}:${internal_port} --name ${DOCKER_IMAGE_NAME} ${DOCKER_IMAGE_URL}"
  echo "${remoteConfig.runCom}"

  sshCommand(
    remote: remoteConfig,
    command: "${remoteConfig.runCom}",
    sudo: false,
  )

  echo "部署成功,请到 http://${REMOTE_HOST}:80 预览效果"
}

Q.E.D.


随意游世