动手造轮子:写一个日志框架

动手造轮子:写一个日志框架

Intro

日志框架有很多,比如 log4net / nlog / serilog / microsoft.extensions.logging 等,如何在切换日志框架的时候做到不用修改代码,只需要切换不同的 loggingProvider 就可以了,最低成本的降低切换日志框架的成本,处于这个考虑自己写了一个日志框架,为不同的日志框架写一个适配,需要用到什么日志框架,配置一下就可以了,业务代码无需变动。

V0

最初的日志强依赖于 log4net,log4net 是我使用的第一个日志框架,所以很长一段时间都在使用它来做日志记录,但是由于是强依赖,在想换日志框架时就会很难受,大量代码要改动,不符合开放封闭的基本原则,于是就有了第一个版本的日志。

V1

第一版的日志参考了微软的日志框架的实现,大概结构如下:

public interface ILogHelperLogFactory
{ILogger CreateLogger(string categoryName);bool AddProvider(ILogHelperProvider provider);
}
public interface ILogHelperLogger
{bool IsEnabled(LogHelperLogLevel logLevel);void Log(LogHelperLogLevel logLevel, Exception exception, string message);
}
public enum LogHelperLogLevel
{/// <summary>/// All logging levels/// </summary>All = 0,/// <summary>/// A trace logging level/// </summary>Trace = 1,/// <summary>/// A debug logging level/// </summary>Debug = 2,/// <summary>/// A info logging level/// </summary>Info = 4,/// <summary>/// A warn logging level/// </summary>Warn = 8,/// <summary>/// An error logging level/// </summary>Error = 16,/// <summary>/// A fatal logging level/// </summary>Fatal = 32,/// <summary>/// None/// </summary>None = 64
}
public interface ILogHelperProvider
{ILogHelperLogger CreateLogger(string categoryName);
}

为了方便 Logger 的使用,定义了一些扩展方法,使得可以直接使用 logger.Info/ logger.Error 等方法,扩展定义如下:

public static void Log(this ILogHelperLogger logger, LogHelperLevel loggerLevel, string msg) => logger.Log(loggerLevel, null, msg);
#region Infopublic static void Info(this ILogHelperLogger logger, string msg, params object[] parameters)
{if (parameters == null || parameters.Length == 0){logger.Log(LogHelperLevel.Info, msg);}else{logger.Log(LogHelperLevel.Info, null, msg.FormatWith(parameters));}
}
public static void Info(this ILogHelperLogger logger, Exception ex, string msg) => logger.Log(LogHelperLevel.Info, ex, msg);
public static void Info(this ILogHelperLogger logger, Exception ex) => logger.Log(LogHelperLevel.Info, ex, ex?.Message);
#endregion Info
// ...其他的类似,这里就不详细展开了

如果要自定义的日志记录的话,就实现一个 ILogHelperProvider 即可,实现一个 ILogHelperProvider 就要实现一个 ILogHelperLogger ,原本强依赖的 log4net 可以实现一个 Log4NetLogHelperProvider,这样换别的日志框架的时候只需要实现对应的 ILogHelperProvider 即可,但是从功能性上来说还是很弱的

如果想要某些日志不记录,比如说,Debug 级别的日志不记录,比如说某一个 Logger 下只记录 Error 级别的日志,现在是有些吃力,只能通过 log4net 的配置来限制了,于是就有了第二个版本,增加了 LoggingFilter 可以针对 Provider/Logger/LogLevel/Exception 来设置 filter,过滤不需要记录的日志,这也是参考了微软的日志框架的 Filter,但是实现不太一样,有兴趣的小伙伴可以自己深入研究一下。

V2

V2 版,在 ILogFactory 的接口上增加了 AddFilter 的方法,定义如下:

/// <summary>    
/// Add logs filter    
/// </summary>    
/// <param name="filterFunc">filterFunc, logProviderType/categoryName/Exception, whether to write log</param>    
bool AddFilter(Func<Type, string, LogHelperLogLevel, Exception, bool> filterFunc);

然后定义了一些扩展方法来方便使用:

public static ILogHelperFactory WithMinimumLevel(this ILogHelperFactory logHelperFactory, LogHelperLevel logLevel)
{return logHelperFactory.WithFilter(level => level >= logLevel);
}
public static ILogHelperFactory WithFilter(this ILogHelperFactory logHelperFactory, Func<LogHelperLevel, bool> filterFunc)
{logHelperFactory.AddFilter((type, categoryName, logLevel, exception) => filterFunc.Invoke(logLevel));return logHelperFactory;
}
public static ILogHelperFactory WithFilter(this ILogHelperFactory logHelperFactory, Func<string, LogHelperLevel, bool> filterFunc)
{logHelperFactory.AddFilter((type, categoryName, logLevel, exception) => filterFunc.Invoke(categoryName, logLevel));return logHelperFactory;
}
public static ILogHelperFactory WithFilter(this ILogHelperFactory logHelperFactory, Func<Type, string, LogHelperLevel, bool> filterFunc)
{logHelperFactory.AddFilter((type, categoryName, logLevel, exception) => filterFunc.Invoke(type, categoryName, logLevel));return logHelperFactory;
}
public static ILogHelperFactory WithFilter(this ILogHelperFactory logHelperFactory, Func<Type, string, LogHelperLevel, Exception, bool> filterFunc)
{logHelperFactory.AddFilter(filterFunc);return logHelperFactory;
}

这样就方便了我们只想定义针对 Logger 的 Filter 以及 Provider 的 Filter,不必所有参数都用到,logging filter 现在已经实现了,此时已经使用了 Serilog 做日志记录有一段时间,感觉 Serilog 里的一些设计很优秀,很优雅,于是想把 Serilog 里的一些设计用在自己的日志框架里,比如说:

  1. Serilog 的扩展叫做 Sink,日志输出的地方, Serilog 自定义一个 Sink,很简单只需要实现一个接口,不需要再实现一个 Logger,从这点来说,我觉得 Serilog 比微软的日志框架更加优秀,而且 LogEvent 使得日志更方便的进行批量操作,有需要的可以了解一下 SerilogPeriodBatching https://github.com/serilog/serilog-sinks-periodicbatching

  2. Serilog 可以自定义一些 Enricher,以此来丰富记录的日志内容,比如日志的请求上下文,日志的环境等,也可以是一些固定的属性信息

  3. MessageTemplate,其实微软的日志框架中也有类似的概念,只不过很不明显,用 Serilog 之前我也很少用,微软的日志框架可以这样用 logger.LogInfo("hello {name}","world") 这样的写法其实就可以把第一个参数当作是 MessageTemplate 或者它内部的叫法 Format

鉴于这么多好处,于是打算将这些功能引入到我的日志框架中

V3

引入 LoggingEvent

说干就干,首先要引入一个 LogHelperLoggingEvent,对应的 SerilogLogEvent,定义如下:

public class LogHelperLoggingEvent : ICloneable
{public string CategoryName { get; set; }public DateTimeOffset DateTime { get; set; }public string MessageTemplate { get; set; }public string Message { get; set; }public Exception Exception { get; set; }public LogHelperLogLevel LogLevel { get; set; }public Dictionary<string, object> Properties { get; set; }public LogHelperLoggingEvent Copy => (LogHelperLoggingEvent)Clone();public object Clone(){var newEvent = (LogHelperLoggingEvent)MemberwiseClone();if (Properties != null){newEvent.Properties = new Dictionary<string, object>();foreach (var property in Properties){newEvent.Properties[property.Key] = property.Value;}}return newEvent;}
}

Event 里定义了一个 Properties 的字典用来丰富日志的内容,另外实现了 ICloneable 接口,方便对对象的拷贝,为了强类型,增加了一个 Copy 的方法,返回一个强类型的对象

改造 LogProvider

为了减少扩展一个 ILogProvider 的复杂性,我们要对 ILogProvider 做一个简化,只需要像扩展 Serilog 的 Sink 一样记录日志即可,不需要关心是否要创建 Logger

改造后的定义如下:

public interface ILogHelperProvider
{Task Log(LogHelperLoggingEvent loggingEvent);
}

(这里返回了一个 Task,可能返回类型是 void 就足够了,看自己的需要)

这样在实现 LogProvider 的时候只需要实现这个接口就可以了,不需要再实现一个 Logger 了

增加 Enricher

Enricher 定义:

public interface ILogHelperLoggingEnricher
{void Enrich(LogHelperLoggingEvent loggingEvent);
}

内置了一个 PropertyEnricher,方便添加一些简单的属性

internal class PropertyLoggingEnricher : ILogHelperLoggingEnricher
{private readonly string _propertyName;private readonly Func<LogHelperLoggingEvent, object> _propertyValueFactory;private readonly bool _overwrite;private readonly Func<LogHelperLoggingEvent, bool> _logPropertyPredict = null;public PropertyLoggingEnricher(string propertyName, object propertyValue, bool overwrite = false) : this(propertyName, (loggingEvent) => propertyValue, overwrite){}public PropertyLoggingEnricher(string propertyName, Func<LogHelperLoggingEvent, object> propertyValueFactory,bool overwrite = false) : this(propertyName, propertyValueFactory, null, overwrite){}public PropertyLoggingEnricher(string propertyName, Func<LogHelperLoggingEvent, object> propertyValueFactory, Func<LogHelperLoggingEvent, bool> logPropertyPredict,bool overwrite = false){_propertyName = propertyName;_propertyValueFactory = propertyValueFactory;_logPropertyPredict = logPropertyPredict;_overwrite = overwrite;}public void Enrich(LogHelperLoggingEvent loggingEvent){if (_logPropertyPredict?.Invoke(loggingEvent) != false){loggingEvent.AddProperty(_propertyName, _propertyValueFactory, _overwrite);}}
}

ILogFactory 增加一个 AddEnricher 的方法

/// <summary>
/// add log enricher
/// </summary>
/// <param name="enricher">log enricher</param>
/// <returns></returns>
bool AddEnricher(ILogHelperLoggingEnricher enricher);

这样我们在记录日志的时候就可以通过这些 Enricher 丰富 LoggingEvent 中的 Properties 了

为了方便 Property 的操作,我们增加了一些扩展方法:

public static ILogHelperFactory WithEnricher<TEnricher>(this ILogHelperFactory logHelperFactory,TEnricher enricher) where TEnricher : ILogHelperLoggingEnricher
{logHelperFactory.AddEnricher(enricher);return logHelperFactory;
}
public static ILogHelperFactory WithEnricher<TEnricher>(this ILogHelperFactory logHelperFactory) where TEnricher : ILogHelperLoggingEnricher, new()
{logHelperFactory.AddEnricher(new TEnricher());return logHelperFactory;
}
public static ILogHelperFactory EnrichWithProperty(this ILogHelperFactory logHelperFactory, string propertyName, object value, bool overwrite = false)
{logHelperFactory.AddEnricher(new PropertyLoggingEnricher(propertyName, value, overwrite));return logHelperFactory;
}
public static ILogHelperFactory EnrichWithProperty(this ILogHelperFactory logHelperFactory, string propertyName, Func<LogHelperLoggingEvent> valueFactory, bool overwrite = false)
{logHelperFactory.AddEnricher(new PropertyLoggingEnricher(propertyName, valueFactory, overwrite));return logHelperFactory;
}
public static ILogHelperFactory EnrichWithProperty(this ILogHelperFactory logHelperFactory, string propertyName, object value, Func<LogHelperLoggingEvent, bool> predict, bool overwrite = false)
{logHelperFactory.AddEnricher(new PropertyLoggingEnricher(propertyName, e => value, predict, overwrite));return logHelperFactory;
}
public static ILogHelperFactory EnrichWithProperty(this ILogHelperFactory logHelperFactory, string propertyName, Func<LogHelperLoggingEvent, object> valueFactory, Func<LogHelperLoggingEvent, bool> predict, bool overwrite = false)
{logHelperFactory.AddEnricher(new PropertyLoggingEnricher(propertyName, valueFactory, predict, overwrite));return logHelperFactory;
}

MessageTemplate

从上面的 LoggingEvent 中已经增加了 MessageTemplate,于是我们引入了微软日志框架中日志的格式化,将 messageTemplate 和 parameters 转换成 Message 和 Properties,具体参考 https://github.com/WeihanLi/WeihanLi.Common/blob/276cc49cfda511f9b7b3bb8344ee52441c4a3b23/src/WeihanLi.Common/Logging/LoggingFormatter.cs

internal struct FormattedLogValue
{public string Msg { get; set; }public Dictionary<string, object> Values { get; set; }public FormattedLogValue(string msg, Dictionary<string, object> values){Msg = msg;Values = values;}
}
internal static class LoggingFormatter
{public static FormattedLogValue Format(string msgTemplate, object[] values){if (values == null || values.Length == 0)return new FormattedLogValue(msgTemplate, null);var formatter = new LogValuesFormatter(msgTemplate);var msg = formatter.Format(values);var dic = formatter.GetValues(values).ToDictionary(x => x.Key, x => x.Value);return new FormattedLogValue(msg, dic);}
}

这样我们就可以支持 messageTemplate 了,然后来改造一下我们的 Logger

public interface ILogHelperLogger
{void Log(LogHelperLogLevel logLevel, Exception exception, string messageTemplate, params object[] parameters);bool IsEnabled(LogHelperLogLevel logLevel);
}

与上面不同的是,我们增加了 parameters

再来更新一下我们的扩展方法,上面的扩展方法是直接使用 string.Format 的方式的格式化的,我们这里要更新一下

public static void Info(this ILogHelperLogger logger, string msg, params object[] parameters)
{logger.Log(LogHelperLogLevel.Info, null, msg, parameters);
}
public static void Info(this ILogHelperLogger logger, Exception ex, string msg) => logger.Log(LogHelperLogLevel.Info, ex, msg);
public static void Info(this ILogHelperLogger logger, Exception ex) => logger.Log(LogHelperLogLevel.Info, ex, ex?.Message);

至此,功能基本完成,但是从 API 的角度来说,感觉现在的 ILogFactory太重了,这些 AddProvider/ AddEnricher/ AddFilter 都应该属性 ILogFactory 的内部属性,通过配置来完成,不应该成为它的接口方法,于是就有了下一版

V4

这一版主要是引入了 LoggingBuilder, 通过 LoggingBuilder 来配置内部的 LogFactory 所需要的 Provider/ Enricher/ Filter,原来他们的配置方法和扩展方法均变成 ILogHelperLoggingBuilder

public interface ILogHelperLoggingBuilder
{/// <summary>/// Adds an ILogHelperProvider to the logging system./// </summary>/// <param name="provider">The ILogHelperProvider.</param>bool AddProvider(ILogHelperProvider provider);/// <summary>/// add log enricher/// </summary>/// <param name="enricher">log enricher</param>/// <returns></returns>bool AddEnricher(ILogHelperLoggingEnricher enricher);/// <summary>/// Add logs filter/// </summary>/// <param name="filterFunc">filterFunc, logProviderType/categoryName/Exception, whether to write log</param>bool AddFilter(Func<Type, string, LogHelperLogLevel, Exception, bool> filterFunc);/ <summary>/ config period batching/ </summary>/ <param name="period">period</param>/ <param name="batchSize">batchSize</param>//void PeriodBatchingConfig(TimeSpan period, int batchSize);/// <summary>/// Build for LogFactory/// </summary>/// <returns></returns>ILogHelperFactory Build();
}

增加 logging 的配置:

public static class LogHelper
{private static ILogHelperFactory LogFactory { get; private set; } = NullLogHelperFactory.Instance;public static void ConfigureLogging(Action<ILogHelperLoggingBuilder> configureAction){var loggingBuilder = new LogHelperLoggingBuilder();configureAction?.Invoke(loggingBuilder);LogFactory = loggingBuilder.Build();}public static ILogHelperLogger GetLogger<T>() => LogFactory.GetLogger(typeof(T));public static ILogHelperLogger GetLogger(Type type) => LogFactory.GetLogger(type);public static ILogHelperLogger GetLogger(string categoryName){return LogFactory.CreateLogger(categoryName);}
}

最后的使用方式:

internal class LoggingTest
{private static readonly ILogHelperLogger Logger = LogHelper.GetLogger<LoggingTest>();public static void MainTest(){var abc = "1233";LogHelper.ConfigureLogging(builder =>{builder.AddLog4Net()//.AddSerilog(loggerConfig => loggerConfig.WriteTo.Console()).WithMinimumLevel(LogHelperLogLevel.Info).WithFilter((category, level) => level > LogHelperLogLevel.Error && category.StartsWith("System")).EnrichWithProperty("Entry0", ApplicationHelper.ApplicationName).EnrichWithProperty("Entry1", ApplicationHelper.ApplicationName, e => e.LogLevel >= LogHelperLogLevel.Error)// 当 LogLevel 是 Error 及以上级别时才增加 Property;});Logger.Debug("12333 {abc}", abc);Logger.Trace("122334334");Logger.Info($"122334334 {abc}");Logger.Warn("12333, err:{err}", "hahaha");Logger.Error("122334334");Logger.Fatal("12333");}
}

More

增加 LoggingEvent 还想做一个批量提交日志,如上面定义的 PeriodBatchingConfig 一样,批量同步到 Provider 但是实际使用下来,有些 provider 不支持设置日志的时间,时间是内部记录的,这样一来日志记录的时间就不准了,而且大多都不支持批量写日志,所以后面放弃了,但是如果只是用自己的扩展,不用 log4net 之类的外部的日志框架的话,我觉得还是可以做的,可以提高效率,目前主要用 Seriloglog4net,暂时不更新了,就先这样吧

下一版本要解决的事情

  • ILogProvider 记录日志返回一个 Task 感觉有些鸡肋,没太大意义,后面再改一下吧

  • Serilog 的 Filter 是基于 LogEvent 的,后面看是否需要改一下,基于 LogEvent 的话更简洁,而且可以根据 LogEvent 内的 Properties 做过滤,所以 AddFilter 的API 可以更新一下 AddFilter(Func<LogHelperLoggingEvent,bool>filter)

Reference

  • https://github.com/serilog/serilog

  • https://github.com/serilog/serilog-sinks-periodicbatching

  • https://github.com/aspnet/Logging

  • https://github.com/aspnet/Extensions/tree/master/src/Logging

  • https://github.com/WeihanLi/WeihanLi.Common/tree/dev/src/WeihanLi.Common/Logging

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/312474.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C】@程序员,我们送给你一个成熟的Excel导入导出组件

程序员的显著特点有一天跟一位同事跟我闲聊&#xff0c;讨论起过去若干年软件行业的感受&#xff0c;他问了个问题&#xff1a;你觉得一个好的软件工程师最显著的特点是什么&#xff1f;我想了一会&#xff0c;说&#xff1a;大概是坐得住吧。某种意义上来说&#xff0c;在互联…

SummerBoot,将SpringBoot的先进理念与C#的简洁优雅合二为一

哈哈哈哈&#xff0c;大家好&#xff0c;我就是高产似母猪的三合&#xff0c;好久没写博客了&#xff0c;因为最近几个月在不断的加班&#xff0c;加班时长平均每个月120小时以上。今天是2020年的第一天&#xff0c;作为一条程序汪&#xff0c;觉得不做点啥好像对不起这个特别有…

C#刷遍Leetcode面试题系列连载(6):No.372 - 超级次方

点击蓝字“dotNET匠人”关注我哟加个“星标★”&#xff0c;每日 7:15&#xff0c;好文必达&#xff01;前文传送门:C# 刷遍 Leetcode 面试题系列连载&#xff08;1&#xff09; - 入门与工具简介C#刷遍Leetcode面试题系列连载&#xff08;2&#xff09;: No.38 - 报数C#刷遍Le…

使用 Postman 测试你的 API

使用 Postman 测试你的 APIIntro最近想对 API 做一些自动化测试&#xff0c;看了几个工具&#xff0c;最后选择了 postman&#xff0c;感觉 postman 的设计更好一些&#xff0c;我们可以在请求发送之前和请求获取到响应之后都可以自定义脚本&#xff0c;很灵活。而且 postman 的…

使用 postman 给 API 写测试

使用 postman 给 API 写测试Intro上次我们简单介绍了 使用 postman 测试 API&#xff0c;这次主要来写一些测试用例以检查请求的响应是否符合我们的预期以及如何使用脚本测试使用 postman 内置的随机变量postman 内置的有一些产生随机值的变量&#xff0c;在发送请求时随机生成…

ASP.NETCore编程实现基本认证

HTTP基本认证在HTTP中&#xff0c;HTTP基本认证&#xff08;Basic Authentication&#xff09;是一种允许浏览器或其他客户端程序使用&#xff08;用户名&#xff0c;口令&#xff09;请求资源的身份验证方式&#xff0c;不要求cookie,session identifier、login page等标记或载…

计算机原理(计算机系统漫游)

计算机五大组成部件&#xff1a;运算器&#xff08;ALU&#xff09;&#xff0c;控制器&#xff0c;存储器&#xff0c;输入部件&#xff0c;输出部件 1.控制器 2.运算器 逻辑运算&#xff08;判断事物的对与错&#xff09; 数学运算(11) 控制器运算器中央处理器&#xff08;CP…

使用ASP.NET Core 3.x 构建 RESTful API - 4.2 过滤和搜索

向Web API传递参数数据可以通过多种方式来传给API。 Binding Source Attributes 会告诉 Model 的绑定引擎从哪里找到绑定源。 共有以下六种 Binding Source Attributes&#xff1a; [FromBody] 请求的 Body [FromForm] 请求的 Body 中的 form数据 [FromHeader] 请求的 Header […

360浏览器linux版本_360安全浏览器崩溃解决方案

360安全浏览器崩溃解决方案方案一&#xff1a;打开360安全浏览器&#xff0c;按键盘上的F1调出浏览器医生界面&#xff0c;点击一键修复即可。如图所示&#xff1a;方案二&#xff1a;360浏览器打开了太多标签占用内存&#xff0c;并且随着浏览器开着的时间越长&#xff0c;占用…

.NET 开源软件开发BIM工具包xBIM

一、xBIM 简介BIM&#xff08;Building Information Modelling&#xff09;建筑信息模型&#xff0c;xBIM&#xff08;eXtensible Building Information Modelling&#xff09;可扩展的建筑信息模型。它是一个.NET 开源软件开发BIM工具包&#xff0c;支持BuildingSmart数据模型…

2019(dotNet全栈开发)公众号回顾

2019年已经过去&#xff0c;人口红利已经逐渐消失&#xff0c;也许这是这10年互联网史上最糟糕的一年&#xff0c;也可能这是未来10年互联网史上最好的一年关于公众号&#xff0c;我其实很早就开始注册了&#xff0c;只是一直没怎么运营&#xff08;ps&#xff1a;不知道怎么推…

CSS padding margin border属性讲解

把所有网页上的对象都放在一个盒&#xff08;box&#xff09;中 &#xff0c;设计师可以通过创建定义来控制这个盒的属性&#xff0c;这些对象包括段落、列表、标题、图片以及层。 盒模型主要定义四个区域&#xff1a; 内容&#xff08;content&#xff09; 内边距&#xff08;…

【实战 Ids4】║ 客户端、服务端、授权中心全线打通!

1、经过元旦两天的全力整改&#xff0c;终于在这新的一年&#xff0c;完成了我的布道生涯的第一个大步走 —— 那就是客户端&#xff08;VUE&#xff09;、服务端&#xff08;ASP.NET Core API&#xff09;、授权中心&#xff08;IdentityServer4&#xff09;的大融合&#xff…

补丁 检测系统_大云制造 | BCLinux For ARM64 V7.6操作系统正式发布

友情提示&#xff1a;全文3000多文字&#xff0c;预计阅读时间10分钟概述大云企业操作系统(BC-Linux&#xff0c;BigCloud Enterprise Linux)是中移(苏州)软件技术有限公司借助开源社区优势&#xff0c;通过定制化手段研发的高性能、安全可靠、自主可控的企业级Linux操作系统。…

告别2019,写给2020:干好技术,要把握好时光里的每一步

本文来自&#xff1a;长沙.NET技术社区 邹溪源不知不觉&#xff0c;一晃年关将近&#xff0c;即将翻开2019&#xff0c;进入新的一页。&#xff08;本文写于2019年12月27日&#xff09;这周已经在朋友圈看到了来自公众号《恰同学少年》《Edi.Wang》和《吃草的罗汉》几位老师写下…

单片机课程设计数字心率计_如何选购合适的PH计

聚舟供应的PH计PH计已经被广泛应用于各个行业&#xff0c;如工业、电力、农业、医药、食品、科研和环保等领域&#xff0c;在酸碱值检测时必不可少的&#xff0c;那么该如何选购呢&#xff1f;聚舟销售的PH计也有多种样式与型号&#xff0c;各种型号都供应充足&#xff0c;欢迎…

cd返回上一 git_如何使用Git实现自动化部署你的项目

在开发过程中&#xff0c;我们不可避免的会用到版本控制。你可能对 Git 和 SVN 有所了解。一开始基本都是在用SVN&#xff0c;现在可能都进化到用Git了吧&#xff0c;因为SVN缺点比较多。这里就不过于多的介绍Git的优点了。不知道大家一开始是怎么使用 git 进行开发的&#xff…

在Asp.Net Core中使用ModelConvention实现全局过滤器隔离

从何说起这来自于我把项目迁移到Asp.Net Core的过程中碰到一个问题。在一个web程序中同时包含了MVC和WebAPI&#xff0c;现在需要给WebAPI部分单独添加一个接口验证过滤器IActionFilter&#xff0c;常规做法一般是写好过滤器后给需要的控制器挂上这个标签&#xff0c;高级点的做…

2 未匹配到任何借口_拼多多【关键词精确匹配溢价】给你想要的精准流量,让你订单暴增的秘诀...

关键词匹配方式升级啦&#xff0c;开启精确匹配溢价功能&#xff0c;拥有更多精准流量。为帮助商家解决在使用多多搜索时&#xff0c;遇到的关键词流量不精准、流量不可控、点击率低的问题&#xff0c;多多搜索新推出【关键词精确匹配溢价】功能。通过精确匹配溢价功能&#xf…