FW+jenkins到Core+Coding的迁移流程记录

_

公司项目是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](https://www.cnblogs.com/vveiliang/p/7409825.html)

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

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

第一步就是新建Core Web项目,在添加推荐取消Https的支持,会有一些新手不会使用,导致无法打开网页(其实打开的时候添加信任证书就行),所以避免麻烦,我一般都是去掉。

而我这边用的Core是3.1版本,Standard是 2.0

而不同库的文件,只要不是特殊文件*.cs一般都直接拷贝过去就行.

但需要注意,Core没Properties.AssemblyInfo.cs,而Web.config对web也有影响,不要多拷,造成项目文件混乱了.

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

文件转移完,我也会对一下,哪些时旧项目多余的文件,引用有没有少文件

尽量都是自己操作,对项目更了解.~~可我这边删的就多了~~

比如项目没有视图,本身只提供Api,那在新的项目中,没必要保留View文件夹,优化好项目目录

这对不是特别大的项目学习有一点的帮助

#### 2.更新必要的引用

接下来,就是先让编译正常,这就要引用必要的nuget包,这里需要注意,Core可以引用FW的包运行,但不能真正去跑一些只有FW的内容

我这边有些包命名有一点的混淆,这样可以通过看下旧项目安装了哪些,跟着装就行。当然,如果你知道哪些是不必须要的,也可以为了项目干净,而不去引用

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

##### 1.HttpContext

Core已经没有了全局的HttpContext,现在能从拦截器或启动时注入获取

一般FW升级到Core都会兼容旧的写法,下面是一种方案

<details>

<summary>HttpContext.cs可以考虑放到Common或者需要的那个库</summary>

```c#

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;

}

}

```

</details>

<details>

<summary>StaticHttpContextExtensions.cs,放到Web项目中</summary>

```c#

/// <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;

}

}

```

</details>

<details>

<summary>Startup.cs</summary>

```c#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

app.UseStaticHttpContext();

}

public void ConfigureServices(IServiceCollection services)

{

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

}

```

</details>

##### 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));

<details>

<summary>EncryptHelper.cs</summary>

```c#

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

}

}

```

</details>

##### 3.FilterAttribute

actionExecutedContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.NotImplemented);

可以参考换成

```c#

actionExecutedContext.Result = new ContentResult

{

StatusCode = (int)HttpStatusCode.NotImplemented,

ContentType = "application/json;charset=utf-8"

};

```

注意新OnActionExecuting入参变化,HttpActionContextActionExecutingContext

原本ActionExecutingContext.Request变成ActionExecutingContext.HttpContext.Request

HeadersContainsContainsKeyGetValues变成直接Headers[""]取值

目前我这边遇到会无法通过编译的不同点就这么多,后续发现再补充

而Core想正常跑起来还是有很多其他的调整,可以继续往下看

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

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

我这边序列化不使用原生System.Text.Json

原因可参考[netcore 3.1 json序列号时间和emoji格式相关问题](https://blog.kagamikun.com/archives/netcore31json%E5%BA%8F%E5%88%97%E5%8F%B7%E6%97%B6%E9%97%B4%E5%92%8Cemoji%E6%A0%BC%E5%BC%8F%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98)

这里加DatetimeJsonConverter 是因Newtonsoft时间转IsoDateTimeConverter设置DefaultDateTimeFormat值得情况下,是直接调用DateTime.ParseExact,如果前端同时会2020-01-012020-01-01 12:00:00两种格式的话,前者会无法反序列化

所以要加一个解析类

<details>

<summary>Startup.cs</summary>

```c#

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

});

```

</details>

<details>

<summary>DatetimeJsonConverter.cs</summary>

```c#

/// <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的实现](/upload/2021/09/image-b7972c44a6f34d92b016ef1ea7710ea6.png)

</details>

##### 2.Controller

Core对控制器的使用跟FW有些不同,需要对控制器添[ApiController]标签,不然打开后,Swagger会出现下面异常

Action require a unique method/path ...

所以推荐用一个BaseControllerClass去做兜底,而其他控制器全部继承它

一来统一了控制器的父类,好做后续子的调整,二来避免改漏

```c#

[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自带的验证返回

```c#

services.Configure<ApiBehaviorOptions>((o) =>

{

o.SuppressModelStateInvalidFilter = true;

});

```

##### 4.DataTime.ToString时间格式

对于Core的时间格式,是获取系统的设置进行格式化的

在一些环境下,时间格式跟win不一样,比如Liunx执DataTime.Now.ToString()出来的就可能5/20/2022 15:00

有些时候我们不能使用这种格式,那么就可以调整这个默认的格式

这里需要注意下,ToString的格式ShortDatePattern+LongTimePattern不要调整错参数了,不然就没效果了

代码如下

<details>

<summary>Program.cs</summary>

```c#

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"

}

};

```

</details>

##### 233.Core 3.1 多斜杠问题

直接就是说3.1不支持多余的斜杠了,忘记N5支不支持,后续我再翻下文档

因为我这边的项目很多其他项目调用,有不确定的多斜杠问题,所以目前解决方式是加管道,移除请求进来时多余的斜杠

需要注意管道添加的位置,最好在进入时处理

<details>

<summary>RewriteRouteRule.cs</summary>

```c#

/// <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;

}

}

}

```

</details>

<details>

<summary>Startup.cs</summary>

```c#

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

app.UseRewriter(new RewriteOptions().Add(RewriteRouteRule.ReWriteRequests));

}

```

</details>

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

进入Coding项目,点击左侧菜代码仓库,再从右上创建代码仓库

记下仓库的地址

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

#### 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看到你刚才提交的代码了

## 四、部署

思路和流程: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创建制品库

在出来的页面输入自己的Coding账号密码,跟Github那种感觉差不多

然后生成自己的令牌,这个需要记一下,后续我们要用

这里也推荐先复制到WSL2里面运行一下,确认返回了登录成功字样:

```

Login Succeeded

```

点击左侧推送,填入我们项目信息,他会帮我们生成命令,我们直接在WSL2执行就行

执行完后,可以执docker images看到我们的镜像

当然在Coding上也能看到推送上去了

#### 2.1添加项目构建计划

这里的计划随便填就行,反正我们后面直接修改脚本内容

重要的是计划名称和代码仓库不错就行。

编译构建使dotnet3.1是因为这是平台构造环境设置的命令

#### 2.2创建项目构建计划

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

#### 2.3设置构造环境

第一步需要先确认我们每个步骤的构造环境,因为流程安排是编译完打包镜像远程推送的,所以全部选择默认构造环境就行

不然错误的环境会导致一些指令在在环境中不一定有,导致整体构造失败

#### 2.4设置编辑流程脚本

在编辑,我们执行两个脚本,一是设置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

```

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

这一步其实没多少参考,大部分需要按照你的情况去编写命令

我这里提供了一份脱敏的脚本,可以参考。

主要流程是登录远程服务器,然后卸载现有docker容器,重新run

<details>

<summary>参考脚本</summary>

```

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 预览效果"

}

```

</details>

OpenVPN 设置非全局代理 2021-09-01
Nginx 快速上手 2021-11-20