公司项目是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。
不过,我这边为了稳,就新建吧。
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都会兼容旧的写法,下面是一种方案
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
Headers
的Contains
变为ContainsKey
,GetValues
变成直接Headers[""]取值
目前我这边遇到会无法通过编译的不同点就这么多,后续发现再补充
而Core想正常跑起来还是有很多其他的调整,可以继续往下看
3.调整Core的依赖注入序列化等基本配置
1.Json序列化编码和过滤器
我这边序列化不使用原生的System.Text.Json
原因可参考netcore 3.1 json序列号时间和emoji格式相关问题
这里加了DatetimeJsonConverter
是因为Newtonsoft
时间转换IsoDateTimeConverter
设置了DefaultDateTimeFormat
值得情况下,是直接调用了DateTime.ParseExact
,如果前端同时会传2020-01-01
和2020-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);
}
}
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项目,点击左侧菜单代码仓库
,再从右上角创建代码仓库
记下仓库的地址
好了,仓库准备完毕,接下来处理代码提交
1.初始化Git
进入项目根目录,执行Git仓库初始化脚本git init
PS C:\**\**> git init
Initialized empty Git repository in C:/**/**/.git/
2.设置源
这里源填刚才创建的仓库的连接
git remote add origin **/**.git
3.添加Git文件和忽略等配置文件(可选)
这里有些公司会有文件忽略要求,推荐在第一次提交前吧这些处理了,如.gitignore
或Docker
文件
当然如果有需要,你可以在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
参考脚本
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.